URL State with searchParams

URL-driven state creates clear causality: tab click → URL changes → Suspense shows skeleton → new data arrives. Users understand what's happening because the URL reflects the current view.

More importantly, URL state is shareable and bookmarkable. /dashboard?filter=drafts&sort=title shows exactly that view—send someone the link and they see what you see.

Reading searchParams

In Server Components, searchParams is a promise that resolves to the current query parameters:

type Props = { searchParams: Promise<{ filter?: string; sort?: string }>; }; export async function PostList({ searchParams }: Props) { const { filter, sort } = await searchParams; // Validate with Zod for type safety const validFilter = filterSchema.parse(filter); const validSort = sortSchema.parse(sort); const posts = await getPosts(validFilter, validSort); return <PostCards posts={posts} />; }

Use Zod schemas with .catch() to provide defaults for invalid or missing params:

const filterSchema = z.enum(['all', 'published', 'drafts', 'archived']).catch('all'); const sortSchema = z.enum(['newest', 'oldest', 'title']).catch('newest');

Updating URL State

In Client Components, use router.push to change the URL:

'use client'; import { useRouter, useSearchParams } from 'next/navigation'; export function PostTabs() { const router = useRouter(); const searchParams = useSearchParams(); const currentFilter = searchParams.get('filter') ?? 'all'; const currentSort = searchParams.get('sort') ?? 'newest'; function handleFilterChange(value: string) { router.push(`/dashboard?filter=${value}&sort=${currentSort}`); } return ( <TabList activeTab={currentFilter} changeAction={handleFilterChange} /> ); }

Optimistic URL Updates

Combine with useOptimistic for instant feedback:

const [optimisticSort, setOptimisticSort] = useOptimistic(currentSort); function handleSortChange(nextSort: string) { startTransition(() => { setOptimisticSort(nextSort); router.push(`/dashboard?filter=${currentFilter}&sort=${nextSort}`); }); }

The tab/button updates instantly; the URL changes; Suspense handles the loading state for the new data.

Benefits Over useState

URL StateComponent State
Shareable via linkLost on refresh
Works with browser historyNo back/forward
Survives refreshResets to default
SEO-friendlyNot indexable

For filter, sort, pagination, and view modes—use URLs. For ephemeral UI state like modal open/close—use component state.

October 15, 2025Updated February 13, 2026324 words