Search Params

Purpose and Scope

Search params in TanStack Router are treated as typed URL state, not as a thin wrapper around the browser URLSearchParams API. The Router guide frames values such as ?page=3 or ?filter-name=tanner as global application state because they survive refreshes, can be shared in links, and let users return to the same filtered or paginated view. The developer problem is that plain platform APIs usually expose strings and flat key-value pairs, while applications often need booleans, numbers, arrays, nested objects, validation, and stable rendering behavior.

Sources: docs/router/guide/search-params.md

TanStack Router's answer is a JSON-first search-param model. The guide explains that the router parses the URL search string into structured JSON so applications can store JSON-serializable values instead of manually encoding everything as strings. This design also separates search updates from pathname updates: changing a page number, filter, sort order, or view mode can be modeled as a state update without pretending that the route path itself changed. In practice, search params become part of the route contract, alongside path params, loader data, and navigation targets.

Sources: docs/router/guide/search-params.md

Relevant Source Files

  • docs/router/guide/search-params.md - Conceptual guide that explains why Router search params go beyond URLSearchParams, how URL state benefits users and developers, and why Router uses JSON-first parsing and serialization.
  • docs/router/api/router/useSearchHook.md - API reference for reading typed search params from components with useSearch, including from, select, strict, shouldThrow, and structuralSharing options.
  • docs/router/api/router/useLoaderDepsHook.md - API reference for reading the loader dependencies that trigger a route loader, including selection and structural sharing options.
  • docs/router/api/router/RouteOptionsType.md - Route option reference that defines validateSearch, SearchSchemaInput, and search.middlewares, which are the route-level APIs that make search params validated and type-aware.

Route-Level Search Contracts

A route declares the shape of its search params with the validateSearch option. The documented contract is that validateSearch receives raw search params from the current location and returns a valid parsed search schema. If validation throws, the route enters an error state and the error is thrown during render; if it succeeds, the return value becomes the route's search params and its type is inferred into the rest of the router. This makes validation both a runtime safety boundary and a TypeScript source for links, navigation, loaders, and component reads.

Sources: docs/router/api/router/RouteOptionsType.md

Defaults and optional inputs are usually part of that validation boundary. The RouteOptions reference documents SearchSchemaInput as a tag that can be applied to the validateSearch parameter type so the input type used by <Link /> and navigate() can differ from the output type consumed by the route. That distinction is useful when callers may omit a search value, but the route wants to consume a normalized result after validation. For example, a page route can accept an optional page query while rendering and loading against a concrete default page number.

Sources: docs/router/api/router/RouteOptionsType.md

import { createFileRoute } from '@tanstack/react-router'
 
type ProductSearchInput = {
  page?: number
  filters?: { category?: string }
}
 
type ProductSearch = {
  page: number
  filters: { category?: string }
}
 
export const Route = createFileRoute('/products')({
  validateSearch: (raw: ProductSearchInput): ProductSearch => {
    return {
      page: raw.page ?? 1,
      filters: raw.filters ?? {},
    }
  },
})

Reading Search Params in Components

Use useSearch when a component needs the current parsed search object for the rendered location. In strict mode, the hook is route-scoped through opts.from, which is documented as the route ID to match search query parameters from. This is the safest mode because TypeScript can infer the specific route search schema instead of collapsing every route's search params into a shared shape. If a component only needs one field, the select option can project the search object and return a narrower value.

Sources: docs/router/api/router/useSearchHook.md

The hook also exposes rendering-oriented controls. shouldThrow defaults to true; when set to false, the hook returns undefined instead of throwing an invariant exception if no match is found in the currently rendered matches. strict: false ignores from and loosens the type to Partial<FullSearchSchema>, which can be useful for shared components that are not tied to a single route. structuralSharing configures whether selected values preserve structural sharing, a detail that matters when complex JSON search state feeds render-sensitive components.

Sources: docs/router/api/router/useSearchHook.md

import { useSearch } from '@tanstack/react-router'
 
function ProductToolbar() {
  const search = useSearch({ from: '/products' })
  const page = useSearch({
    from: '/products',
    select: (search) => search.page,
  })
 
  return <div>Page {page}</div>
}

Loader Dependencies and Search-Driven Data

Search params often decide what a loader should fetch: a list page might depend on pagination, filters, sorting, or a selected tab. The useLoaderDeps hook documents the read side of that model by returning the dependencies used to trigger a route's loader. The hook accepts from, which is the route ID or path to get loader dependencies from, and returns either the dependency object or a selected value. This keeps components aligned with the same dependency object that controls loader execution.

Sources: docs/router/api/router/useLoaderDepsHook.md

Use loader dependencies when search params should participate in cache keys, reload decisions, or UI labels that explain what data is currently shown. A route can validate and normalize search params first, derive loader dependencies from that normalized state, and then fetch data based on those dependencies. Components can read the final search state with useSearch and the loader dependency projection with useLoaderDeps, avoiding duplicate parsing logic. The select and structuralSharing options on useLoaderDeps mirror the component ergonomics documented for useSearch.

Sources: docs/router/api/router/useLoaderDepsHook.md, docs/router/api/router/useSearchHook.md

import { useLoaderDeps } from '@tanstack/react-router'
 
function ProductResultsHeader() {
  const category = useLoaderDeps({
    from: '/products',
    select: (deps) => deps.category,
  })
 
  return <h2>{category ? `Category: ${category}` : 'All products'}</h2>
}

Once search params are part of the route contract, navigation APIs can treat them as typed state transitions. The RouteOptions reference documents search.middlewares as functions that transform search parameters when generating new links for a route or its descendants. A middleware receives the current search and a next function, then returns the transformed search. This is useful for cross-cutting URL-state rules such as preserving a tenant, stripping transient filters, or normalizing search values before a link is emitted.

Sources: docs/router/api/router/RouteOptionsType.md

The guide's motivation is important here: search params change frequently and independently from the pathname. That means a good Router app should avoid hand-building query strings and should instead express URL updates through typed link or navigation options. Validation defines what can enter the route, middleware defines how search state is transformed for generated links, and hooks define how rendered components consume the result. Together, those pieces let URL state behave more like application state while still remaining shareable, bookmarkable, and reload-safe.

Sources: docs/router/guide/search-params.md, docs/router/api/router/RouteOptionsType.md

Compact API Reference

APIWhere it is definedPurposeKey options or behavior
validateSearchRouteOptionsParse and validate raw search params for a matched routeReturns TSearchSchema; throws put the route into an error state; return type is inferred into the router
SearchSchemaInputRouteOptionsType the accepted search input separately from the validated route outputUseful when links and navigate() may accept optional fields that validation normalizes
search.middlewaresRouteOptionsTransform search params when generating links for a route or descendantsReceives { search, next } and returns a TSearchSchema
useSearchRouter hookRead current search query parameters as an objectSupports from, select, shouldThrow, strict, and structuralSharing
useLoaderDepsRouter hookRead dependencies that trigger a route loaderSupports from, select, and structuralSharing

Practical Next Steps

Start by deciding which parts of a screen's state must survive refresh, sharing, and back/forward navigation. Put those values in the route's search schema, validate them with validateSearch, and normalize defaults there instead of scattering fallback logic through components. Then read search state with useSearch, derive data-fetching inputs through loader dependencies, and use useLoaderDeps where components need to display or select those dependency values. For deeper follow-up, read the pages on data loading, custom search serialization, complex search params, link/navigation APIs, and render optimizations.