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

  • nuqs provides 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