React cookbook · Beginner
Sync filters and pagination with the URL
Make a filtered product list shareable and keep filters when the user refreshes or goes back.
Approach
Treat search params as the source of truth: read filters with useSearchParams, validate them, and update them with setSearchParams (replace for typing, push for discrete changes). Use the parsed params in the query key.
Solution
import { useSearchParams } from "react-router";
import { useQuery } from "@tanstack/react-query";
const SORTS = ["newest", "price-asc", "price-desc"] as const;
type Sort = (typeof SORTS)[number];
export function useProductFilters() {
const [params, setParams] = useSearchParams();
const filters = {
q: params.get("q") ?? "",
sort: (SORTS as readonly string[]).includes(params.get("sort") ?? "") ? (params.get("sort") as Sort) : "newest",
page: Math.max(1, Number(params.get("page")) || 1),
};
function update(patch: Partial<typeof filters>, opts?: { replace?: boolean }) {
setParams((prev) => {
const next = new URLSearchParams(prev);
for (const [k, v] of Object.entries(patch)) {
if (v === "" || v === undefined || (k === "page" && v === 1)) next.delete(k); else next.set(k, String(v));
}
if (!("page" in patch)) next.delete("page"); // reset page when filters change
return next;
}, opts);
}
return { filters, update };
}
export function Products() {
const { filters, update } = useProductFilters();
const { data } = useQuery({ queryKey: ["products", filters], queryFn: () => fetchProducts(filters) });
return (
<>
<input value={filters.q} onChange={(e) => update({ q: e.target.value }, { replace: true })} />
<select value={filters.sort} onChange={(e) => update({ sort: e.target.value as Sort })}>
{SORTS.map((s) => <option key={s}>{s}</option>)}
</select>
<ProductGrid items={data?.items ?? []} />
</>
);
}
Pitfalls
- Mirroring URL state in useState (they drift).
- Trusting raw params without validation.
- Pushing a history entry per keystroke.
Variations
nuqsprovides type-safe search-param state hooks.- TanStack Router validates search params with schemas.
Further reading: React Router: useSearchParams
Reference: https://reactrouter.com/api/hooks/useSearchParams
More React recipes
- Add optimistic updates to a todo list
- Build an accessible modal dialog
- Build infinite scroll with TanStack Query
- Call a cookie-authenticated API with CSRF protection
- Debounce a search input in React
- Handle errors gracefully with error boundaries
- Implement dark mode that follows the system
- Poll a long-running job's status