One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

useSearchParams

Without a schema, returns the route’s path and query parameters as a read-only URLSearchParams object. This provides the standard web API for reading the combined parameters, rather than parsing only the URL’s query string.

Basic Usage

import { useSearchParams } from 'one'
// URL: /products?sort=price&category=electronics
export default function Products() {
const searchParams = useSearchParams()
const sort = searchParams.get('sort') // 'price'
const category = searchParams.get('category') // 'electronics'
const missing = searchParams.get('missing') // null
return <Text>Sorted by {sort}</Text>
}

URLSearchParams Methods

The returned object supports all standard URLSearchParams read methods:

const searchParams = useSearchParams()
// Get a single value
searchParams.get('key') // string | null
// Check if param exists
searchParams.has('key') // boolean
// Get all values for a key (for repeated params)
searchParams.getAll('tag') // string[]
// Iterate over all entries
for (const [key, value] of searchParams) {
console.log(key, value)
}
// Convert to string
searchParams.toString() // 'key=value&other=123'

Repeated Parameters

Use getAll() when a parameter appears multiple times:

// URL: /filter?tag=react&tag=typescript&tag=native
const searchParams = useSearchParams()
const tags = searchParams.getAll('tag')
// tags = ['react', 'typescript', 'native']

Typed Search Params

Pass a Standard Schema to read the query as typed values and get a function that updates it. Use a compatible schema from Zod, Valibot, ArkType, Effect’s Standard Schema adapter, or another library that implements the standard.

import { useSearchParams } from 'one'
import { z } from 'zod'
export const tableParams = z.object({
page: z.coerce.number().default(1),
tags: z.preprocess(
(value) => (Array.isArray(value) ? value : value === undefined ? [] : [value]),
z.array(z.string())
),
})
export function Table() {
const [{ page, tags }, update] = useSearchParams(tableParams)
// page is a number, tags is a string[]
return (
<>
<Button onPress={() => update({ page: page + 1 })}>Next</Button>
<Button onPress={() => update({ tags: null }, { replace: true })}>Clear tags</Button>
</>
)
}

The schema receives every query key as a string, or as an array of strings when the key repeats, so a schema that wants an array should accept both forms. Only keys that are in the route’s query are included. Path params (including a catch-all) and the hash are left out, whether the hook is in a page or in a layout. The { global } option works the same as above.

Updating

update(partial, { replace? }) merges into the current URL and navigates with router.push, or router.replace with { replace: true }. Path params, other query keys and the hash are kept. A value of null or undefined removes that key. A screen that is mounted but not focused, such as a hidden tab, writes to its own route and does not carry the focused screen’s hash.

A schema cannot turn a value back into text, so values are written by one rule: a string, number or boolean is written as is, and an array of those becomes a repeated key. For a field whose output is not one of these (a Date for instance) the update takes the schema’s input type, which is the text the schema reads. Anything else throws a TypeError.

Invalid URLs

If the schema rejects the URL, useSearchParams throws a SearchValidationError while rendering, so the nearest error boundary handles it and the rest of the page is unaffected. Schemas that use .default or .catch read a bad URL as a good one.

Validation is synchronous. A schema that returns a promise throws a TypeError. Earlier drafts let an async schema pass silently.

One finds the schemas that your components pass to useSearchParams while it generates routes.d.ts. Link then checks searchParams against the destination route:

<Link href="/products" searchParams={{ page: 2, tags: ['a', 'b'] }}>Page 2</Link>
// type errors: misspelled key, wrong value type
<Link href="/products" searchParams={{ pge: 2 }}>...</Link>
<Link href="/products" searchParams={{ page: 'two' }}>...</Link>

A route’s keys come from the schemas used by that route file, its layouts, and everything they import through relative or tsconfig path imports. Two components on one route that use the same key get the intersection of their types.

Discovery reads source text and does not run your code, so a schema has to be an exported const (or imported from one) for One to see it. A call it cannot follow, such as an inline schema, is reported once in the terminal and leaves that route’s links untyped. Dynamic href strings are untyped as well.

Keys are plain and flat (?page=2), and arrays are repeated keys (?tags=a&tags=b).

Validated search params in loaders

The same schemas also validate the query on the server before a loader runs. One finds the schemas the way it does for typed links, in the route file, its layouts and what they import, and hands them to that route’s loader. A loader then receives the schemas’ output as searchParams:

app/products.tsx
import { useLoader, type RouteType } from 'one'
import { ProductTable } from '~/features/ProductTable' // calls useSearchParams(tableParams)
export async function loader({ searchParams }: RouteType<'/products'>['LoaderProps']) {
// searchParams.page is a number, as the schema produces it
return getProducts(searchParams?.page)
}

There is nothing to merge or register. Each schema validates its own copy of the original query, and the outputs are merged into one object. If a schema rejects the query, the server answers 400 with the issue messages and the loader does not run. If two schemas produce different values for the same key, or a schema returns something other than an object, the server answers 500 instead of picking one.

Where it applies:

URL encoding of arrays

An array search param is now written as repeated keys everywhere the router builds a URL, because that is the only form that reads back as the same array. Before, router.push({ pathname, params: { tags: ['a', 'b'] } }) could not round-trip through the address bar. This is a visible URL change for apps that pass arrays as search params, so it ships in a minor release.

Arrays in a Link’s href.params still join with commas. For example, href={{ pathname: '/products', params: { tags: ['a', 'b'] } }} produces ?tags=a%2Cb, which reads back as the single string 'a,b'. This also loses the distinction between commas inside a value and commas between values. Use searchParams={{ tags: ['a', 'b'] }} on Link to produce repeated keys, ?tags=a&tags=b, which useSearchParams().getAll('tags') reads as ['a', 'b'].

Typed Link discovery reads paths from your tsconfig and from a tsconfig it extends by relative path, but not from one it extends from a package.

Global Option

By default, useSearchParams only updates when the current route is focused. Pass { global: true } to update on any route change:

// Updates even when this screen is not focused
const searchParams = useSearchParams({ global: true })
The global option cannot change between renders.

Read-Only

The returned URLSearchParams is read-only. Calling set(), append(), or delete() will throw an error:

const searchParams = useSearchParams()
// ❌ These will throw
searchParams.set('key', 'value')
searchParams.append('key', 'value')
searchParams.delete('key')

To update search params, use the router:

import { useRouter, useSearchParams } from 'one'
function Filters() {
const router = useRouter()
const searchParams = useSearchParams()
const updateSort = (sort: string) => {
// Build new search string
const params = new URLSearchParams(searchParams)
params.set('sort', sort)
router.push(`/products?${params.toString()}`)
}
}

useSearchParams vs useParams

HookReturnsUse for
useParamsPlain objectQuick access to params as object
useSearchParams() without a schemaURLSearchParamsStandard web API, repeated params

Both useParams() and useSearchParams() without a schema include path params and query params. The return type changes how you read them, not which params are included:

// Route: /users/[id], URL: /users/alice?tab=profile
const { id, tab } = useParams()
// id === 'alice', tab === 'profile'
const searchParams = useSearchParams()
searchParams.get('id') // 'alice'
searchParams.get('tab') // 'profile'

If a query key has the same name as a dynamic path parameter, the path value takes precedence. For /users/alice?id=bob&tab=profile, both hooks above still return id as 'alice'; the query value 'bob' is not separately available through them. Use distinct names when you need both values.

The schema overload, useSearchParams(schema), keeps only keys present in the query before validating them. A path-only id is excluded, but a query key named id still receives the merged path value when the names collide.

Expo Router Compatibility

For easier migration from Expo Router, the following aliases are available (but deprecated):

Prefer the non-aliased names in new code.

Edit this page on GitHub.