React Router 08 ⚛️ Query Parameters — Managing search parameters
The previous chapter covered path parameters: the dynamic segments in the URL path that identify a resource (/users/:id). This chapter covers query parameters: the key-value pairs in the URL search string that configure the view (?sort=name&page=2). The LFCS exam does not test React Router, but front-end engineering certification paths do. The critical distinction is between the path parameter and the query parameter. The path parameter is part of the resource identity. The query parameter is the optional configuration: the filter, the sort, the pagination, and the tab selection. The useSearchParams hook is the interface. It returns a read-only URLSearchParams object and a setter function. The setter updates the URL, and the router re-renders the component with the new search params. The critical trap is the URLSearchParams mutation: the object is read-only; the set and delete methods on the hook’s setter are the correct way to update it. A direct params.set("key", "value") on the read-only object is a no-op.
Key point: The useSearchParams hook returns [searchParams, setSearchParams]. The searchParams is a URLSearchParams object with get, getAll, has, and entries. The setSearchParams is a function that accepts a new URLSearchParams, an object, or a function that receives the current params and returns the new ones. The replace option controls whether the navigation replaces or pushes the history entry. The preventScrollReset option prevents the scroll-to-top on the navigation.
Why query parameters require a specific mental model
The string problem. The URLSearchParams object returns the values as strings, just like the path parameters. The ?page=2 returns the "2" string, not the 2 number. The component that does arithmetic on the value must parse it. The component that passes it to the array index must parse it. The Number(searchParams.get("page")) is the conversion.
The multi-value problem. A query parameter can appear multiple times: ?tag=javascript&tag=react&tag=node. The searchParams.get("tag") returns only the first value. The searchParams.getAll("tag") returns an array of all values. The component that uses get for the multi-value parameter is the component that loses the data.
The update problem. The searchParams object is read-only. The searchParams.set("page", "2") is a method call on the URLSearchParams object, and it mutates the object. But the mutation does not trigger a re-render, and the URL is not updated. The setSearchParams is the function that updates the URL and triggers the re-render. The correct pattern is setSearchParams(prev => { prev.set("page", "2"); return prev; }) or setSearchParams({ page: "2" }).
The history problem. The setSearchParams pushes a new history entry by default. The user can press the back button to return to the previous search. The { replace: true } option replaces the current entry, which is the correct choice for the filter that should not pollute the history. The pagination is the push; the filter is the replace.
The scroll problem. A navigation resets the scroll position to the top by default. The { preventScrollReset: true } option prevents this, which is the correct choice for the pagination and the filter that should not jump to the top. The ScrollRestoration component in the React Router 6.4+ is the alternative.
The trade-off. The query parameter is the correct choice for the optional configuration. The path parameter is the correct choice for the resource identity. The query parameter is bookmarkable, shareable, and back-button-friendly. The component state is faster but not shareable. The URL is the source of truth.
a. Reading the query parameters
The useSearchParams hook returns the current params and the setter.
import { useSearchParams } from "react-router";
function ProductList() {
const [searchParams] = useSearchParams();
const sort = searchParams.get("sort") || "name";
const page = Number(searchParams.get("page") || "1");
return (
<div>
<p>Sorting by: {sort}</p>
<p>Page: {page}</p>
</div>
);
}
The searchParams.get("sort") returns the string or null. The || "name" fallback is the default. The Number(searchParams.get("page") || "1") parses the page number with the default.
The searchParams.has("key") returns a boolean. The searchParams.getAll("key") returns an array of all values for the multi-value parameter.
const tags = searchParams.getAll("tag");
// ["javascript", "react", "node"]
The searchParams.entries() returns an iterator of [key, value] pairs.
for (const [key, value] of searchParams.entries()) {
console.log(key, value);
}
b. Updating the query parameters
The setSearchParams function updates the URL and triggers the re-render. It accepts an object, a URLSearchParams, or a function.
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();
const handleSort = (newSort) => {
setSearchParams({ sort: newSort, page: "1" });
};
return (
<button onClick={() => handleSort("price")}>
Sort by price
</button>
);
}
The object form replaces the entire search string. The setSearchParams({ sort: "price", page: "1" }) sets the URL to ?sort=price&page=1. The other params are removed. The function form preserves the existing params.
const handleSort = (newSort) => {
setSearchParams((prev) => {
prev.set("sort", newSort);
prev.set("page", "1");
return prev;
});
};
The prev.set mutates the current params, and the function returns the mutated object. The other params are preserved. This is the correct pattern for the filter that does not remove the unrelated params.
The { replace: true } option replaces the history entry.
setSearchParams({ sort: "price" }, { replace: true });
The { preventScrollReset: true } option prevents the scroll-to-top.
setSearchParams({ page: "2" }, { preventScrollReset: true });
c. The validation and the route-level search schema
The query parameters are strings. The component that uses them as numbers, booleans, or enums must parse and validate them. The zod library is the common choice, and the route-level validateSearch is the integration point.
import { z } from "zod";
const searchSchema = z.object({
sort: z.enum(["name", "price", "date"]).default("name"),
page: z.coerce.number().int().positive().default(1),
tags: z.array(z.string()).default([]),
});
const router = createBrowserRouter([
{
path: "products",
Component: ProductList,
validateSearch: searchSchema,
},
]);
The validateSearch parses the search params and returns the typed object. The component reads the parsed values with the useSearchParams hook, which returns the parsed object when the route has the validateSearch.
The useSearchParams returns the URLSearchParams object by default. The route’s validateSearch is the mechanism for the typed access. The useSearchParams in the TanStack Router is the typed access; the React Router’s validateSearch is the equivalent.
In React Router, the useSearchParams does not have the typed access built in. The component uses the zod schema explicitly.
function ProductList() {
const [searchParams] = useSearchParams();
const result = searchSchema.safeParse(Object.fromEntries(searchParams));
if (!result.success) {
return <p>Invalid search params</p>;
}
const { sort, page, tags } = result.data;
return <div>...</div>;
}
The Object.fromEntries(searchParams) converts the URLSearchParams to a plain object. The safeParse validates the object. The result.data is the typed object. The result.success check is the validation.
Complete Example Session
# ============================================
# PART 1: READ PARAMS
# ============================================
const [searchParams] = useSearchParams();
const sort = searchParams.get("sort") || "name";
# ============================================
# PART 2: PARSE NUMBER
# ============================================
const page = Number(searchParams.get("page") || "1");
# ============================================
# PART 3: MULTI-VALUE
# ============================================
const tags = searchParams.getAll("tag");
// ["javascript", "react"]
# ============================================
# PART 4: UPDATE WITH OBJECT
# ============================================
const [, setSearchParams] = useSearchParams();
setSearchParams({ sort: "price", page: "1" });
# ============================================
# PART 5: UPDATE WITH FUNCTION
# ============================================
setSearchParams((prev) => {
prev.set("page", "2");
return prev;
});
# ============================================
# PART 6: REPLACE HISTORY
# ============================================
setSearchParams({ sort: "price" }, { replace: true });
# ============================================
# PART 7: PREVENT SCROLL RESET
# ============================================
setSearchParams({ page: "2" }, { preventScrollReset: true });
# ============================================
# PART 8: VALIDATE WITH ZOD
# ============================================
const schema = z.object({
sort: z.enum(["name", "price"]).default("name"),
page: z.coerce.number().default(1),
});
const result = schema.safeParse(Object.fromEntries(searchParams));
# ============================================
# PART 9: VERIFY
# ============================================
# /products?sort=price&page=2 → sort=price, page=2
# setSearchParams({sort: "date"}) → /products?sort=date
# getAll("tag") → all values
The nine parts trace the query parameter flow: read, parse, multi-value, update object, update function, replace, prevent scroll, validate, and verification.
Quick Reference
useSearchParams
| Return | Type | Purpose |
|---|---|---|
searchParams | URLSearchParams | Read |
setSearchParams | function | Update |
Read Methods
| Method | Returns |
|---|---|
get(key) | String or null |
getAll(key) | Array of strings |
has(key) | Boolean |
entries() | Iterator of [key, value] |
Update Forms
| Form | Effect |
|---|---|
setSearchParams({ k: v }) | Replace all params |
setSearchParams(prev => ...) | Preserve and update |
{ replace: true } | Replace history |
{ preventScrollReset: true } | No scroll to top |
Validation
| Tool | Purpose |
|---|---|
z.coerce.number() | Parse string to number |
safeParse | Return result object |
Object.fromEntries(searchParams) | Convert to object |
Best Practices
✅ Do This:
const page = Number(searchParams.get("page") || "1");
<!-- Parse string to number --> # ✅
setSearchParams(prev => { prev.set("page", "2"); return prev; });
<!-- Preserve existing params --> # ✅
setSearchParams({ page: "2" }, { replace: true });
<!-- Replace history for filters --> # ✅
❌ Don’t Do This:
searchParams.set("page", "2")
<!-- No re-render, no URL update --> # ❌
array[searchParams.get("page")]
<!-- String index on array --> # ❌
setSearchParams({ page: "2" })
# For a filter
<!-- Adds a new history entry --> # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| URL not updating | Mutated searchParams directly | Use setSearchParams |
| Page number wrong | String used as number | Number(...) |
| Only first tag | Used get | Use getAll |
| Back button issue | Filter pushed history | replace: true |
| Scroll jumps | Pagination reset | preventScrollReset: true |
Real-World Examples
1. Read Param
const sort = searchParams.get("sort");
2. Parse Number
const page = Number(searchParams.get("page") || "1");
3. Read All
const tags = searchParams.getAll("tag");
4. Update Object
setSearchParams({ sort: "price" });
5. Update Function
setSearchParams(prev => { prev.set("page", "2"); return prev; });
6. Replace History
setSearchParams({ sort: "price" }, { replace: true });
7. Prevent Scroll
setSearchParams({ page: "2" }, { preventScrollReset: true });
8. Zod Validate
z.object({ page: z.coerce.number() }).safeParse(Object.fromEntries(searchParams));
9. Reset Filter
setSearchParams({});
10. Delete Key
setSearchParams(prev => { prev.delete("sort"); return prev; });
Visual
URL to Params
┌──────────────────────────────────────────────┐
│ URL: /products?sort=price&page=2&tag=a&tag=b│
└──────────────────┬───────────────────────────┘
│
│ useSearchParams
▼
┌──────────────────────────────────────────────┐
│ searchParams.get("sort") → "price" │
│ searchParams.get("page") → "2" │
│ searchParams.getAll("tag") → ["a", "b"] │
└──────────────────────────────────────────────┘
Update Flow
┌──────────────────────────────────────────────┐
│ setSearchParams({ sort: "price" }) │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ URL updated: ?sort=price │
│ Router re-renders │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Component reads new params │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Read hook | useSearchParams() |
| Setter | setSearchParams(...) |
| Read single | get(key) |
| Read all | getAll(key) |
| Check existence | has(key) |
| Replace history | { replace: true } |
| Prevent scroll | { preventScrollReset: true } |
| Validation | zod + Object.fromEntries |
Key takeaways:
- The
useSearchParamsreturns the read-only object and the setter. ThesearchParamsis theURLSearchParams. ThesetSearchParamsis the function. The direct mutation ofsearchParamsis a no-op. - The values are strings. The
get("page")returns"2", not2. TheNumber(...)or thez.coerce.number()parses the value. - The
getAllis for the multi-value parameters. Theget("tag")returns only the first value. ThegetAll("tag")returns the array. - The
setSearchParamsaccepts an object or a function. The object form replaces all params. The function form preserves the existing params and updates the specified ones. - The
{ replace: true }is for the filters. The filter should not add a new history entry. The pagination should. The{ preventScrollReset: true }prevents the scroll-to-top.
Remember: The query parameter is the configuration. The path parameter is the identity. The useSearchParams is the read. The setSearchParams is the write. The get is the single. The getAll is the multi. The replace is the filter. The preventScrollReset is the pagination. The exam tests the string parsing and the setter.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!