URL Search Params as State

Filters Belong in the URL

Filters, sort order, page number and the selected tab should live in the URL: users can share and bookmark them, the back button works, and Server Components can read them via searchParams.

'use client';

import { usePathname, useRouter, useSearchParams } from 'next/navigation';

export default function SortSelect() {
  const router = useRouter();
  const pathname = usePathname();
  const params = useSearchParams();

  function onChange(value: string) {
    const next = new URLSearchParams(params);
    next.set('sort', value);
    next.delete('page'); // new sort -> back to page 1
    router.replace(pathname + '?' + next.toString(), { scroll: false });
  }

  return (
    <select value={params.get('sort') ?? 'relevance'} onChange={(e) => onChange(e.target.value)}>
      <option value="relevance">Relevance</option>
      <option value="price_asc">Price: Low to High</option>
      <option value="price_desc">Price: High to Low</option>
    </select>
  );
}

Parsing Filters Safely

Example

The server page turns searchParams into a typed filter object, with defaults and limits:

type Sort = 'relevance' | 'price_asc' | 'price_desc';
type Filters = { q: string; sort: Sort; page: number; brands: string[]; maxPrice: number | null };

const SORTS: Sort[] = ['relevance', 'price_asc', 'price_desc'];

function parseFilters(sp: Record<string, string | string[] | undefined>): Filters {
  const one = (k: string) => (Array.isArray(sp[k]) ? sp[k]![0] : sp[k]) ?? '';
  const sort = SORTS.includes(one('sort') as Sort) ? (one('sort') as Sort) : 'relevance';
  const page = Math.min(Math.max(parseInt(one('page'), 10) || 1, 1), 100);
  const brands = ([] as string[]).concat(sp.brand ?? []).filter(Boolean);
  const max = Number(one('maxPrice'));
  return { q: one('q').trim(), sort, page, brands, maxPrice: max > 0 ? max : null };
}

console.log(parseFilters({ q: ' phone ', sort: 'price_asc', page: '3', brand: ['redmi', 'realme'], maxPrice: '20000' }));
console.log(parseFilters({ sort: 'cheapest', page: '-4', brand: 'samsung' }));

Output

{
  q: 'phone',
  sort: 'price_asc',
  page: 3,
  brands: [ 'redmi', 'realme' ],
  maxPrice: 20000
}
{
  q: '',
  sort: 'relevance',
  page: 1,
  brands: [ 'samsung' ],
  maxPrice: null
}