URL State with searchParams
URL search parameters provide shareable, bookmarkable state that persists across refreshes.
Reading searchParams
From app/dashboard/_components/PostList.tsx:
const filterSchema = z.enum(['all', 'published', 'drafts', 'archived']).catch('all');
const sortSchema = z.enum(['newest', 'oldest', 'title']).catch('newest');
export async function PostList({ searchParams }) {
const { filter, sort } = await searchParams;
const validFilter = filterSchema.parse(filter);
const validSort = sortSchema.parse(sort);
const posts = await getPosts(validFilter, validSort);
// ...
}Updating URL State
From app/dashboard/_components/PostTabs.tsx:
'use client';
export function PostTabs() {
const searchParams = useSearchParams();
const router = useRouter();
const currentTab = searchParams.get('filter') ?? 'all';
const currentSort = searchParams.get('sort') ?? 'newest';
function tabAction(value: string) {
// Preserve other params when updating one
router.push(`/dashboard?filter=${value}&sort=${currentSort}`);
}
return <TabList activeTab={currentTab} changeAction={tabAction} />;
}Cycle Button with Optimistic State
From app/dashboard/_components/SortButton.tsx—a button that cycles through options:
'use client';
const sortOptions = [
{ icon: ArrowUpDown, label: 'Newest', value: 'newest' },
{ icon: ArrowDownUp, label: 'Oldest', value: 'oldest' },
{ icon: ArrowDownAZ, label: 'Title', value: 'title' },
];
export function SortButton() {
const searchParams = useSearchParams();
const router = useRouter();
const currentSort = searchParams.get('sort') ?? 'newest';
const currentFilter = searchParams.get('filter') ?? 'all';
const [optimisticSort, setOptimisticSort] = useOptimistic(currentSort);
const [isPending, startTransition] = useTransition();
const currentIndex = sortOptions.findIndex(opt => opt.value === optimisticSort);
const nextIndex = (currentIndex + 1) % sortOptions.length;
const nextSort = sortOptions[nextIndex].value;
function sortAction() {
startTransition(() => {
setOptimisticSort(nextSort);
router.push(`/dashboard?filter=${currentFilter}&sort=${nextSort}`);
});
}
return (
<Button onClick={sortAction} disabled={isPending}>
{sortOptions[currentIndex].label}
</Button>
);
}URL state works with browser history and makes pages shareable—/dashboard?filter=drafts&sort=title shows exactly that view.