| | |

React Router 08 ⚛️ Query Parameters — Managing search parameters

⏱️ Estimated read: 9 min | |

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

ReturnTypePurpose
searchParamsURLSearchParamsRead
setSearchParamsfunctionUpdate

Read Methods

MethodReturns
get(key)String or null
getAll(key)Array of strings
has(key)Boolean
entries()Iterator of [key, value]

Update Forms

FormEffect
setSearchParams({ k: v })Replace all params
setSearchParams(prev => ...)Preserve and update
{ replace: true }Replace history
{ preventScrollReset: true }No scroll to top

Validation

ToolPurpose
z.coerce.number()Parse string to number
safeParseReturn 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

PitfallWhy It HappensFix
URL not updatingMutated searchParams directlyUse setSearchParams
Page number wrongString used as numberNumber(...)
Only first tagUsed getUse getAll
Back button issueFilter pushed historyreplace: true
Scroll jumpsPagination resetpreventScrollReset: 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

ItemValue
Read hookuseSearchParams()
SettersetSearchParams(...)
Read singleget(key)
Read allgetAll(key)
Check existencehas(key)
Replace history{ replace: true }
Prevent scroll{ preventScrollReset: true }
Validationzod + Object.fromEntries

Key takeaways:

  • The useSearchParams returns the read-only object and the setter. The searchParams is the URLSearchParams. The setSearchParams is the function. The direct mutation of searchParams is a no-op.
  • The values are strings. The get("page") returns "2", not 2. The Number(...) or the z.coerce.number() parses the value.
  • The getAll is for the multi-value parameters. The get("tag") returns only the first value. The getAll("tag") returns the array.
  • The setSearchParams accepts 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.

Was this helpful?


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!