← Back
Edit Post
Title
Description
searchParams for shareable state, Zod validation, optimistic URL updates.
Content
Markdown supported
# 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: ```tsx 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: ```tsx 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: ```tsx '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: ```tsx 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 State | Component State | |-----------|-----------------| | Shareable via link | Lost on refresh | | Works with browser history | No back/forward | | Survives refresh | Resets to default | | SEO-friendly | Not indexable | For filter, sort, pagination, and view modes—use URLs. For ephemeral UI state like modal open/close—use component state.
Published
Save Changes
Cancel