Careeroza
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
}