← Back

URL State with searchParams

Shareable filter/sort state, preserving params on update, cycle button with optimistic UI.

PublishedMarch 5, 2026254 words
Edit

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); // ... }
const filterSchema = z.enum(['all', 'published', 'drafts', 'archived']).catch('all'); const sortSchema = z.enum(['newest', 'oldest',

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'

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:

URL state works with browser history and makes pages shareable—/dashboard?filter=drafts&sort=title shows exactly that view.

'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
)
;
// ...
}
)
??
'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
}
/>
;
}
'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} />; }
'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
>
)
;
}
'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> ); }