Route Params and Search Params as State
IntermediatePath params identify a resource, search params describe a view. Treating search params as your filter state makes every view shareable and refresh-proof for free.
Overview
A URL is state that the browser persists, the user can copy, and the back button already knows how to undo. Most React apps under-use it — filters, sorting, tabs and pagination sit in useState, so a shared link shows something different from what the sender was looking at, and a refresh drops everything. The rule that resolves it is simple: anything that changes what is on screen and should survive a reload belongs in the URL. Only genuinely ephemeral state — a dropdown being open, a draft mid-typing — stays local.
Path Params
They identify the thing. Read them, validate them, and handle the missing case.
{ path: 'problems/:slug' }
const { slug } = useParams() // always a string, never typed
// The URL is user input. Anyone can type /problems/../../etc
const { data, error } = useQuery({
queryKey: ['problem', slug],
queryFn: () => getProblem(slug),
})
if (error?.status === 404) return <NotFound />
// Numeric ids need parsing and checking
const id = Number(useParams().id)
if (!Number.isInteger(id)) return <NotFound />
// Optional and catch-all segments
{ path: 'problems/:slug?' }
{ path: 'docs/*' } // useParams()['*'] holds the rest
// A changing param does NOT remount the component by default —
// the same route matched, so state persists across the navigation.
// Key on it when a fresh start is what you want:
<ProblemDetail key={slug} />Search Params as Filter State
Read them like state, write them like navigation.
const [searchParams, setSearchParams] = useSearchParams()
const topic = searchParams.get('topic') ?? 'all'
const page = Number(searchParams.get('page') ?? 1)
const sort = searchParams.get('sort') ?? 'newest'
function setFilter(key, value) {
setSearchParams(prev => {
const next = new URLSearchParams(prev) // preserve the other params
value ? next.set(key, value) : next.delete(key)
next.delete('page') // reset paging on any filter
return next
}, { replace: true }) // do not stack a history entry
}
// replace: true for filter tweaks — otherwise ten adjustments mean
// ten presses of back to leave the page. Use a real push for
// navigation the user would expect to undo.
// The query then follows the URL automatically, with no extra wiring
useQuery({ queryKey: ['problems', { topic, page, sort }], ... })
// Multi-select filters
searchParams.getAll('tag') // ['arrays', 'graphs']What Does Not Belong in the URL
The line, and the privacy rule that decides part of it.
// In the URL — visible, shareable, refresh-proof
// filters, sort, page, tab, search query, opened item id,
// a modal that deserves its own address ("/problems/x?share=1")
// In component state — nobody wants to share these
// a dropdown being open, hover state, an in-progress draft,
// scroll position, whether a tooltip has been dismissed
// NEVER in the URL — it lands in server logs, browser history,
// referrer headers and analytics:
// tokens, passwords, OTPs, email addresses, phone numbers,
// anything personally identifying
// URLs get long. Encode properly and keep keys short:
next.set('q', 'two sum & more') // URLSearchParams encodes for you
// A shared helper keeps parsing consistent across the app
function useFilters() {
const [params, setParams] = useSearchParams()
return {
topic: params.get('topic') ?? 'all',
page: Math.max(1, Number(params.get('page') ?? 1)),
setFilter: (k, v) => { ... },
}
}Key Points to Remember
- 1Path params identify a resource; search params describe how the view is filtered or sorted
- 2Params arrive as strings and are user input — parse, validate and handle the invalid case
- 3Preserve other search params when setting one, and reset the page whenever a filter changes
- 4Use replace for filter adjustments so the back button leaves the page rather than undoing ten tweaks
- 5Never put tokens or personal data in a query string — it is logged, cached and sent in referrers
Interview Questions
Sign in to ask AriaWhy put filter and pagination state in the URL instead of useState?
Why use replace rather than push when updating a filter?
What should never go into a query string, and why?
Ask Aria about Route Params and Search Params as State
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.