Search params
Route params tell your app which page it's on. Search params, also called query params, describe how that page should look: which filter is active, what the list is sorted by, which page of results is showing. They live in the URL after a question mark, as key/value pairs like /vans?type=rugged, with extra pairs joined by ampersands: /vans?type=rugged&sort=price. Because they're part of the URL, they survive a refresh and travel inside a shared link, which makes them a different kind of state than anything useState can hold.
State that belongs in the URL
State kept with useState lives in memory. Refresh the page and it resets to its initial value; copy the URL to a friend and they get a fresh start with none of your choices. For a lot of state that's exactly right. For some of it, it's a real loss: if you've narrowed a list of vans down to the rugged ones under a certain price, you probably want a refresh to keep that view, and you want a pasted link to open the same curated list for someone else.
The course offers a useful test: should a user be able to revisit or share this page exactly as it is and get the same result? If yes, consider raising that piece of state out of React and into the URL as a search param. Filtering, sorting, and pagination are the classic candidates. The URL then becomes the single source of truth for that state, and your component derives what it renders from it, the same way it would derive from state or props.
Reading params with useSearchParams
React Router exposes the query string through the useSearchParams hook, and its shape is deliberately close to useState: an array holding the current value and a setter.
import { useSearchParams } from 'react-router-dom'
export default function CharacterList() {
const [searchParams, setSearchParams] = useSearchParams()
const typeFilter = searchParams.get('type')
// ...
}searchParams is an instance of the browser's native URLSearchParams object rather than a plain object, so you interact with it through its methods. .get('type') returns the value of the type param as a string, and returns null when that param isn't in the URL at all. That null is how your code knows no filter is active. .toString() serializes the whole set back into a query string like type=sith&sort=price, without the leading question mark.
Version note
The examples here import from react-router-dom, which works in both v6 and v7; Routing covers what v7 changed about the packages.
Filtering a list from a param
With the param in hand, filtering is plain JavaScript at the top of the component. No state, no effect: reading a param and filtering an array are both fast, so it's fine to redo the work on every render and let the result fall out of the URL.
const typeFilter = searchParams.get('type')
const displayedCharacters = typeFilter
? characters.filter(char => char.type.toLowerCase() === typeFilter.toLowerCase())
: characters
const charEls = displayedCharacters.map(char => (
<li key={char.name}>{char.name}</li>
))The ternary handles the no-filter case: when .get returns null, the full list is shown. Choosing which array to render this way is the same derive-as-you-render move you've used for conditional rendering, driven by the URL instead of by state. Watch for casing mismatches too; the data stores "Sith" while a hand-edited link might carry Sith or sith, so both sides get lowercased before they're compared.
Setting params with Links
The most direct way to put a search param in the URL is a Link whose to starts with a question mark. React Router sees the leading ?, keeps you on the current route, and swaps in the new query string, which re-renders the component and re-runs your filtering.
<Link to="?type=jedi">Jedi</Link>
<Link to="?type=sith">Sith</Link>
<Link to=".">Clear</Link>For clearing, to="." navigates to the current path with no query attached; to="" works as well, and the course settles on the dot as the more explicit of the two. Links fit best when the filter is a fixed set of visible choices: they render as real anchor tags, so users can open a filtered view in a new tab or copy the address before clicking.
Setting params with the setter function
The second element from useSearchParams is a setter, and like the one from useState it accepts either a replacement value or a callback. Since a button isn't part of React Router's ecosystem the way Link is, you call the setter from an ordinary event handler.
<button onClick={() => setSearchParams({ type: 'jedi' })}>Jedi</button>
<button onClick={() => setSearchParams({ type: 'sith' })}>Sith</button>
<button onClick={() => setSearchParams({})}>Clear</button>The setter is flexible about what it takes: a string like '?type=jedi' (with or without the question mark) works, but the object form shown here is what you'll see most often, and an empty object clears everything. Reach for the setter when the new params come out of logic instead of a click on a fixed choice: reading values from a form, responding to input as the user types, or setting several params at once.
Replacing wipes the other params
Both approaches so far hard-code the entire query string. That's fine while type is the only param your app uses, but the moment the URL also carries something unrelated, say ?name=jill&type=jedi, clicking one of those links or buttons replaces the whole query string and name=jill is gone. The clear buttons are even blunter: they wipe every param in the URL, including ones this component never touched. If you're certain your project will only ever have the one param, hard-coding is fine. Otherwise you want to merge.
Merging with existing params
For Links, the to prop takes a string, so the fix is a small helper that runs during render: copy the current params into a fresh URLSearchParams, change the one key that's moving, and serialize the result. It lives inside the component, since it reads searchParams from the hook.
// inside CharacterList, so it can read searchParams
function genNewSearchParamString(key, value) {
const sp = new URLSearchParams(searchParams)
if (value === null) {
sp.delete(key)
} else {
sp.set(key, value)
}
return `?${sp.toString()}`
}<Link to={genNewSearchParamString('type', 'jedi')}>Jedi</Link>
<Link to={genNewSearchParamString('type', 'sith')}>Sith</Link>
<Link to={genNewSearchParamString('type', null)}>Clear</Link>This is vanilla JavaScript rather than anything React Router provides. The URLSearchParams constructor happily accepts an existing params object as its starting point, .set updates or adds one key, and passing null signals the helper to .delete the key instead, so "Clear" now removes only type and leaves everything else in the URL intact.
For the setter, use its callback form. The callback receives the previous params object, you adjust the one key, and return it.
// inside CharacterList, so it can call setSearchParams
function handleFilterChange(key, value) {
setSearchParams(prevParams => {
if (value === null) {
prevParams.delete(key)
} else {
prevParams.set(key, value)
}
return prevParams
})
}<button onClick={() => handleFilterChange('type', 'jedi')}>Jedi</button>
<button onClick={() => handleFilterChange('type', null)}>Clear</button>One surprise the course calls out: unlike a useState updater, where mutating the previous state is forbidden, here it's fine to call .delete and .set directly on prevParams and return it. Now both the links and the buttons change only the param they own.
useSearchParams lets you read it: searchParams.get('type') returns the value, or null when the param isn't there. You can set params with a Link to a query string or with the setter function, then filter your list from whatever the URL says.
Next up: Protected routes, where a branch of the app asks who you are first.

