Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/agents/tsh-software-engineer.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ When working from a `*.plan.md` file — whether implementing the full plan or a
- `tsh-sql-and-database-understanding` - when writing SQL queries, designing database schemas, creating migrations, implementing ORM-based data access, optimising query performance, or working with transactions and locking. Applies to PostgreSQL, MySQL, MariaDB, SQL Server, and Oracle.
- `tsh-implementing-frontend` - for UI tasks: component patterns, composition, design tokens, barrel files, and Figma-to-code workflow.
- `tsh-implementing-forms` - for form tasks: schema validation, field composition, error handling, multi-step form flows.
- `tsh-implementing-filters` - for filter tasks: type-safe URL filter synchronization, bracket notation serialization, filter sync hooks, push/replace navigation strategies.
- `tsh-writing-hooks` - for custom hooks: naming, composition, stable returns, effect cleanup, testing.
- `tsh-ensuring-accessibility` - for WCAG 2.1 AA compliance: semantic HTML, ARIA, keyboard navigation, focus management, screen readers.
- `tsh-optimizing-frontend` - for frontend performance: code splitting, memoization, bundle size, rendering optimization, memory management.
Expand Down
330 changes: 330 additions & 0 deletions .github/skills/tsh-implementing-filters/SKILL.md

Large diffs are not rendered by default.

111 changes: 111 additions & 0 deletions .github/skills/tsh-implementing-filters/references/nextjs-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Next.js Filter Patterns

Next.js App Router-specific patterns for the tsh-implementing-filters skill. Load this reference when the project uses Next.js.

## Table of Contents

- [Reading Search Params](#reading-search-params)
- [Navigation (Push/Replace)](#navigation-pushreplace)
- [Server Component Constraints](#server-component-constraints)
- [Bracket Notation with Next.js](#bracket-notation-with-nextjs)
- [Anti-Patterns](#anti-patterns)

## Reading Search Params

Next.js App Router provides different APIs for reading URL search params depending on the context:

| Context | API | Notes |
| ---------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Client Component | `useSearchParams()` from `next/navigation` | Returns `ReadonlyURLSearchParams`. Only available in Client Components. |
| Server Component | `searchParams` prop from page/layout (async in Next.js 15+ — must be `await`ed) | Passed as a prop to `page.tsx`. Access `searchParams.filter` etc. |
| Route Handler | `request.nextUrl.searchParams` | In `route.ts` handlers. |

Key difference from React Router: `useSearchParams()` returns a **read-only** object. You cannot call `setSearchParams()` — you must construct a new URL and navigate.

## Navigation (Push/Replace)

How to navigate with updated filters in Next.js App Router:

```typescript
import { useRouter, usePathname, useSearchParams } from "next/navigation";

const useFilterNavigation = () => {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();

const navigate = (
newParams: URLSearchParams,
options?: { replace?: boolean },
) => {
const url = `${pathname}?${newParams.toString()}`;
if (options?.replace) {
router.replace(url);
} else {
router.push(url);
}
};

return { navigate, searchParams, pathname };
};
```

`router.push()` and `router.replace()` use **soft navigation** by default — no full page reload, which is the desired behavior for filter changes.

## Server Component Constraints

- Server Components **cannot** use `useSearchParams()` — they receive `searchParams` as a page prop.
- Deserialization logic (bracket notation parsing, type coercion) works identically in both contexts — only the source of raw params differs.
- Pattern: Define serialization/deserialization as plain functions (not hooks). The `useFilters` hook wraps them for Client Components. Server Components call the deserialization function directly.

```typescript
// Shared — works in both contexts
const deserializeFilters = (params: URLSearchParams): ProductFilters => {
/* ... */
};

// Client Component — via hook
const useFilters = (defaults: ProductFilters) => {
const searchParams = useSearchParams();
const filters = deserializeFilters(
new URLSearchParams(searchParams.toString()),
);
// ...
};

// Server Component — direct call
export default async function ProductsPage({
searchParams,
}: {
searchParams: Promise<Record<string, string | string[]>>;
}) {
const resolvedParams = await searchParams;
const params = new URLSearchParams();
// Convert resolvedParams record to URLSearchParams...
const filters = deserializeFilters(params);
// ...
}
```

## Bracket Notation with Next.js

Next.js does not natively parse bracket notation (`filter[color]=blue`) into structured objects. The `searchParams` prop gives flat key-value pairs where the key is the literal string `filter[color]`.

The deserialization function must parse bracket keys manually. This logic is framework-agnostic — identical to the React Router approach. Only the source of `URLSearchParams` differs.

```typescript
// searchParams in Next.js page: { "filter[color]": "blue", "filter[tags]": ["a", "b"] }
// Parse bracket keys to extract field names
```

## Anti-Patterns

| Anti-Pattern | Instead Do |
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| Using `useSearchParams()` in Server Components | Use the `searchParams` page prop in Server Components |
| Calling `setSearchParams()` like React Router | Construct new URL string, use `router.push()` / `router.replace()` |
| Wrapping entire page in Client Component for filter access | Keep filter display in Server Components via `searchParams` prop; only filter controls need Client Component |
| Using `router.push()` with full URL including origin | Use pathname-relative URLs: `` `${pathname}?${params}` `` |
| Using nested objects for range filters (e.g., `{ price: { min, max } }`) | Use flat properties (`priceMin`, `priceMax`) so partial updates with shallow spread merge correctly |

> **Note:** When composing filter update functions, use flat properties for ranges (e.g., `priceMin`, `priceMax` instead of nested `{ price: { min, max } }`) so that partial updates with shallow spread merge correctly.
147 changes: 147 additions & 0 deletions .github/skills/tsh-implementing-filters/references/react-patterns.md
Comment thread
marekgieldatsh marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# React Filter Patterns

React Router v6+-specific patterns for the `tsh-implementing-filters` skill. Load this reference when the project uses React Router.

## Table of Contents

- [Reading Search Params](#reading-search-params)
- [Navigation (Push/Replace)](#navigation-pushreplace)
- [Hook Binding](#hook-binding)
- [Bracket Notation Parsing](#bracket-notation-parsing)
- [Anti-Patterns](#anti-patterns)

## Reading Search Params

| API | Usage | Notes |
| -------------------------- | ----------------------------------------- | ---------------------------------------------------------------------- |
| `useSearchParams()` | Returns `[searchParams, setSearchParams]` | Read-write tuple. `searchParams` is a `URLSearchParams` instance. |
| `searchParams.get(key)` | Single value | Returns `string \| null` |
| `searchParams.getAll(key)` | Multi-value (arrays) | Returns `string[]` — use for repeated bracket keys like `filter[tags]` |
| `searchParams.toString()` | Full query string | For URL construction |

Key difference from Next.js: React Router's `useSearchParams()` returns a **read-write** tuple — you can use `setSearchParams()` directly to update the URL without constructing a full URL string.

## Navigation (Push/Replace)

**Approach 1: `setSearchParams()` (preferred for filter updates)**

```typescript
import { useSearchParams } from "react-router-dom";

const useFilterNavigation = () => {
const [searchParams, setSearchParams] = useSearchParams();

const navigate = (
newParams: URLSearchParams,
options?: { replace?: boolean },
) => {
setSearchParams(newParams, { replace: options?.replace });
};

return { navigate, searchParams };
};
```

`setSearchParams` also supports a functional updater for deriving from the previous params:

```typescript
setSearchParams(
(prev) => {
const next = new URLSearchParams(prev);
next.set("filter[color]", "blue");
return next;
},
{ replace: true },
);
```

**Approach 2: `useNavigate()` (when path + search need to change together)**

```typescript
import { useNavigate, useSearchParams } from "react-router-dom";

const useFilterNavigation = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();

const navigateWithFilters = (
newParams: URLSearchParams,
options?: { replace?: boolean; pathname?: string },
) => {
navigate(
{ pathname: options?.pathname ?? ".", search: newParams.toString() },
{ replace: options?.replace },
);
};

return { navigateWithFilters, searchParams };
};
```

Use `setSearchParams()` when only filters change. Use `useNavigate()` when both the path segment and query string need to update simultaneously (e.g., changing category + resetting filters). `setSearchParams()` with `{ replace: true }` maps directly to the skill's navigation strategy — use it for search-as-you-type, omit it (defaults to push) for discrete filter actions.

## Hook Binding

Concrete React Router binding for the generic `useFilters` hook described in SKILL.md Step 4:

```typescript
import { useSearchParams } from "react-router-dom";

const useFilters = <T extends Record<string, unknown>>({
defaults,
serialize,
deserialize,
}: {
defaults: T;
serialize: (filters: T) => URLSearchParams;
deserialize: (params: URLSearchParams) => T;
}) => {
const [searchParams, setSearchParams] = useSearchParams();

const filters = deserialize(searchParams);

const updateFilters = (
partial: Partial<T>,
options?: { replace?: boolean },
) => {
setSearchParams(
(prev) => {
const current = deserialize(prev);
const next = { ...current, ...partial };
return serialize(next);
},
{ replace: options?.replace },
);
};

const resetFilters = () => {
setSearchParams(serialize(defaults));
};

return { filters, updateFilters, resetFilters };
};
```

The hook reads `searchParams` for the current filter state and uses `setSearchParams` with a functional updater for writes. No `useEffect` or `useState` needed.

Note: `{ ...filters, ...partial }` is a shallow merge. Use flat properties for ranges (e.g., `priceMin`, `priceMax` instead of nested `{ price: { min, max } }`) so that partial updates merge correctly.

## Bracket Notation Parsing

React Router does not parse bracket notation (`filter[color]=blue`) into structured objects — the `searchParams` instance treats `filter[color]` as a literal key string. The deserialization function must parse bracket keys manually. This logic is framework-agnostic. See SKILL.md Step 3 for the serialization/deserialization pattern.

```typescript
// searchParams.get('filter[color]') → 'blue'
// searchParams.getAll('filter[tags]') → ['a', 'b']
// Parse bracket keys to extract field names and structure
```

## Anti-Patterns

| Anti-Pattern | Instead Do |
| ------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Using `useNavigate()` when only filters change | Use `setSearchParams()` — simpler, doesn't require path construction |
| Mutating the `searchParams` object directly and expecting re-render | Create new `URLSearchParams` via `serialize()`, pass to `setSearchParams()` |
| Using `setSearchParams` with string instead of `URLSearchParams` | Pass `URLSearchParams` object for type safety and correct encoding |
| Wrapping `setSearchParams` in `useEffect` to sync state | Call `setSearchParams` directly in event handlers — no effect sync needed |
| Using `navigate(-1)` to "undo" filter changes | Rely on browser Back button — push navigation already handles this |
1 change: 1 addition & 0 deletions .github/skills/tsh-implementing-forms/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,4 @@ Form:
- `tsh-ensuring-accessibility` — for WCAG compliance in form fields, labels, and error announcements
- `tsh-writing-hooks` — for custom form-related hooks/composables (useFormField, useMultiStepForm)
- `tsh-reviewing-frontend` — for form-specific review criteria during code review
- `tsh-implementing-filters` — for filter UIs that wire form inputs into URL-synced filter hooks
1 change: 1 addition & 0 deletions .github/skills/tsh-implementing-frontend/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,5 +148,6 @@ The patterns above are framework-agnostic. For framework-specific implementation
- `tsh-ensuring-accessibility` — to ensure components meet WCAG 2.1 AA standards
- `tsh-optimizing-frontend` — for performance considerations during component implementation
- `tsh-implementing-forms` — for form-specific component patterns and validation
- `tsh-implementing-filters` — for URL filter synchronization patterns consumed by list and search components
- `tsh-writing-hooks` — for custom hook patterns used within components
- `tsh-reviewing-frontend` — for frontend-specific code review of implemented components
1 change: 1 addition & 0 deletions .github/skills/tsh-writing-hooks/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,4 @@ The patterns above are framework-agnostic. For framework-specific hook/composabl
- `tsh-optimizing-frontend` — for memoization strategies and performance patterns in hooks
- `tsh-reviewing-frontend` — for hook-specific code review criteria
- `tsh-implementing-forms` — for form-related custom hooks (field state, validation triggers)
- `tsh-implementing-filters` — for the filter sync hook pattern (useFilters) that applies hook composition conventions
35 changes: 35 additions & 0 deletions website/docs/skills/implementing-filters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
sidebar_position: 29
Comment thread
marekgieldatsh marked this conversation as resolved.
title: Implementing Filters
---

# Implementing Filters

**Folder:** `.github/skills/tsh-implementing-filters/`
**Used by:** Software Engineer

Type-safe URL filter synchronization with clean path and query string routing. Headless filter logic — schema definition, bracket notation serialization, URL sync hooks, and navigation strategy patterns.

Comment thread
marekgieldatsh marked this conversation as resolved.
## Key Areas

| Area | Coverage |
| ----------------------- | ---------------------------------------------------------------------------------------- |
| **Filter Schema** | TypeScript types, defaults, supported param types (single, multi-select, range, boolean) |
| **URL Structure** | Path-vs-query golden rule, bracket notation (`filter[field]=value`) |
| **Serialization** | Serialize/deserialize functions, bracket notation, snake_case keys, default omission |
| **Filter Sync Hook** | `useFilters` hook — typed state from URL, `updateFilters`, `resetFilters` |
| **Navigation Strategy** | Push vs replace per action type, debounced inputs, search-as-you-type |
| **API Contract** | TSH bracket notation default, external API adaptation, response envelope |

## When to Use

- Implementing filterable lists, search pages, or faceted navigation
- Persisting filter state in the URL for shareable links and back/forward navigation
- Building filter sync hooks that derive state from URL search params
- Adapting serialization for external APIs with non-bracket conventions

## Connected Skills

- `tsh-implementing-frontend` — component patterns that consume headless filter logic
- `tsh-implementing-forms` — form-based filter UIs
- `tsh-writing-hooks` — hook composition patterns for the filter sync hook
2 changes: 1 addition & 1 deletion website/docs/skills/optimizing-frontend.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 29
sidebar_position: 30
title: Frontend Optimization
---

Expand Down
Loading