
search-params
validateSearch, search param validation with Zod/Valibot/ArkType adapters, fallback(), search middlewares (retainSearchParams, stripSearchParams), custom serialization (parseSearch, stringifySearch), search param inheritance, loaderDeps for cache keys, reading and writing search params.
- Installs
- 204
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/tanstack/router --skill search-params- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads search-params automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
Search Params
TanStack Router treats search params as JSON-first application state. They are automatically parsed from the URL into structured objects (numbers, booleans, arrays, nested objects) and validated via validateSearch on each route.
CRITICAL: When using
zodValidator()and Zod v3, usefallback()from@tanstack/zod-adapter, NOT zod's.catch(). Using.catch()with the zod adapter makes the output typeunknown, destroying type safety. This does not apply to Valibot or ArkType (which use their own fallback mechanisms). It also does not apply to Zod v4, which should use.catch()and not use thezodValidator(). CRITICAL: Types are fully inferred. Never annotate the return ofuseSearch().
Setup: Zod Adapter (Recommended)
npm install zod @tanstack/zod-adapter
// src/routes/products.tsx
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
const productSearchSchema = z.object({
page: z.number().default(1).catch(1),
filter: z.string().default(''),
sort: z.enum(['newest', 'oldest', 'price']).default('newest').catch('newest'),
})
export const Route = createFileRoute('/products')({
validateSearch: productSearchSchema,
component: ProductsPage,
})
function ProductsPage() {
// page: number, filter: string, sort: 'newest' | 'oldest' | 'price'
// ALL INFERRED — do not annotate
const { page, filter, sort } = Route.useSearch()
return (
<div>
<p>
Page {page}, filter: {filter}, sort: {sort}
</p>
</div>
)
}
Reading Search Params
In Route Components: Route.useSearch()
function ProductsPage() {
const { page, sort } = Route.useSearch()
return <div>Page {page}</div>
}
In Code-Split Components: getRouteApi()
import { getRouteApi } from '@tanstack/react-router'
const routeApi = getRouteApi('/products')
function ProductFilters() {
const { sort } = routeApi.useSearch()
return <select value={sort}>{/* options */}</select>
}
From Any Component: useSearch({ from })
import { useSearch } from '@tanstack/react-router'
function SortIndicator() {
const { sort } = useSearch({ from: '/products' })
return <span>Sorted by: {sort}</span>
}
Loose Access: useSearch({ strict: false })
function GenericPaginator() {
const search = useSearch({ strict: false })
// search.page is number | undefined (union of all routes)
return <span>Page: {search.page ?? 1}</span>
}
Writing Search Params
Link with Function Form (Preserves Existing Params)
import { Link } from '@tanstack/react-router'
function Pagination() {
return (
<Link
from="/products"
search={(prev) => ({ ...prev, page: prev.page + 1 })}
>
Next Page
</Link>
)
}
Link with Object Form (Replaces All Params)
<Link to="/products" search={{ page: 1, filter: '', sort: 'newest' }}>
Reset
</Link>
Programmatic: useNavigate()
import { useNavigate } from '@tanstack/react-router'
function SortDropdown() {
const navigate = useNavigate({ from: '/products' })
return (
<select
onChange={(e) => {
navigate({
search: (prev) => ({ ...prev, sort: e.target.value, page: 1 }),
})
}}
>
<option value="newest">Newest</option>
<option value="price">Price</option>
</select>
)
}
Search Param Inheritance
Parent route search params are automatically merged into child routes:
// src/routes/shop.tsx — parent defines shared params
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
const shopSearchSchema = z.object({
currency: z.enum(['USD', 'EUR']).default('USD').catch('USD'),
})
export const Route = createFileRoute('/shop')({
validateSearch: shopSearchSchema,
})
// src/routes/shop/products.tsx — child inherits currency
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/shop/products')({
component: ShopProducts,
})
function ShopProducts() {
// currency is available here from parent — fully typed
const { currency } = Route.useSearch()
return <div>Currency: {currency}</div>
}
Search Middlewares
retainSearchParams — Keep Params Across Navigation
import { createRootRoute, retainSearchParams } from '@tanstack/react-router'
import { z } from 'zod'
const rootSearchSchema = z.object({
debug: z.boolean().optional(),
})
export const Route = createRootRoute({
validateSearch: rootSearchSchema,
search: {
middlewares: [retainSearchParams(['debug'])],
},
})
stripSearchParams — Remove Default Values from URL
import { createFileRoute, stripSearchParams } from '@tanstack/react-router'
import { z } from 'zod'
const defaults = { sort: 'newest', page: 1 }
const searchSchema = z.object({
sort: z.string().default(defaults.sort),
page: z.number().default(defaults.page),
})
export const Route = createFileRoute('/items')({
validateSearch: searchSchema,
search: {
middlewares: [stripSearchParams(defaults)],
},
})
Chaining Middlewares
export const Route = createFileRoute('/search')({
validateSearch: z.object({
retainMe: z.string().optional(),
arrayWithDefaults: z.string().array().default(['foo', 'bar']),
required: z.string(),
}),
search: {
middlewares: [
retainSearchParams(['retainMe']),
stripSearchParams({ arrayWithDefaults: ['foo', 'bar'] }),
],
},
})
Custom Serialization
Override the default JSON serialization at the router level:
import {
createRouter,
parseSearchWith,
stringifySearchWith,
} from '@tanstack/react-router'
const router = createRouter({
routeTree,
// Example: use JSURL2 for compact, human-readable URLs
parseSearch: parseSearchWith(parse),
stringifySearch: stringifySearchWith(stringify),
})
Using Search Params in Loaders via loaderDeps
export const Route = createFileRoute('/products')({
validateSearch: productSearchSchema,
// Pick ONLY the params the loader needs — not the entire search object
loaderDeps: ({ search }) => ({ page: search.page }),
loader: async ({ deps }) => {
return fetchProducts({ page: deps.page })
},
})
Common Mistakes
1. HIGH: Using zod v3's .catch() with zodValidator() instead of adapter fallback()
// WRONG — .catch() with zodValidator makes the type unknown
const schema = z.object({ page: z.number().catch(1) })
validateSearch: zodValidator(schema) // page is typed as unknown!
// CORRECT — fallback() preserves the inferred type
import { fallback } from '@tanstack/zod-adapter'
const schema = z.object({ page: fallback(z.number(), 1) })
Important: This only applies when using Zod v3, not when using Zod v4. For v4, using .catch() is correct.
2. HIGH: Returning entire search object from loaderDeps
// WRONG — loader re-runs on ANY search param change
loaderDeps: ({ search }) => search
// CORRECT — loader only re-runs when page changes
loaderDeps: ({ search }) => ({ page: search.page })
3. HIGH: Passing Date objects in search params
// WRONG — Date does not serialize correctly to JSON in URLs
<Link search={{ startDate: new Date() }}>
// CORRECT — convert to ISO string
<Link search={{ startDate: new Date().toISOString() }}>
4. MEDIUM: Parent route missing validateSearch blocks inheritance
// WRONG — child cannot access shared params
export const Route = createRootRoute({
component: RootComponent,
// no validateSearch!
})
// CORRECT — parent must define validateSearch for children to inherit
export const Route = createRootRoute({
validateSearch: globalSearchSchema,
component: RootComponent,
})
5. HIGH (cross-skill): Using search as object instead of function loses params
// WRONG — replaces ALL search params, losing any existing ones
<Link to="." search={{ page: 2 }}>Page 2</Link>
// CORRECT — preserves existing params, updates only page
<Link to="." search={(prev) => ({ ...prev, page: 2 })}>Page 2</Link>
References
- Validation Patterns Reference — comprehensive patterns for all validation libraries
FAQ
What does the search-params skill do?
validateSearch, search param validation with Zod/Valibot/ArkType adapters, fallback(), search middlewares (retainSearchParams, stripSearchParams), custom serialization (parseSearch, stringifySearch), search param inheritance, loaderDeps for cache keys, reading and writing search params.
How do I install search-params?
Run npx skills add https://github.com/tanstack/router --skill search-params in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/search-params/ (personal) or .claude/skills/search-params/ (project).
Which AI agents can use search-params?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is search-params safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains search-params?
It is published by tanstack in tanstack/router. It has 204 recorded installs.
More from tanstack/router
router-core
tanstack/router
Framework-agnostic core concepts for TanStack Router: route trees, createRouter, createRoute, createRootRoute, createRootRouteWithContext, addChildren, Register type declaration, route matching, route sorting, file naming conventions. Entry point for all router skills.
react-router
tanstack/router
React bindings for TanStack Router: RouterProvider, useRouter, useRouterState, useMatch, useMatches, useLocation, useSearch, useParams, useNavigate, useLoaderData, useLoaderDeps, useRouteContext, useBlocker, useCanGoBack, Link, Navigate, Outlet, CatchBoundary, ErrorComponent. React-specific patterns for hooks, providers, SSR hydration, and createLink with forwardRef.
react-start
tanstack/router
React bindings for TanStack Start: createStart, StartClient, StartServer, React-specific imports, re-exports from @tanstack/react-router, full project setup with React, useServerFn hook.
start-core
tanstack/router
Core overview for TanStack Start: tanstackStart() Vite plugin, getRouter() factory, root route document shell (HeadContent, Scripts, Outlet), client/server entry points, routeTree.gen.ts, tsconfig configuration. Entry point for all Start skills.
router-query
tanstack/router
Integrating TanStack Router with TanStack Query: queryClient in router context, static queries and fire-and-forget queries in loaders, useSuspenseQuery in components, defaultPreloadStaleTime: 0, setupRouterSsrQueryIntegration for SSR dehydration/hydration and streaming, per-request QueryClient isolation.
start-server-core
tanstack/router
Server-side runtime for TanStack Start: createStartHandler, request/response utilities (getRequest, setResponseHeader, setCookie, getCookie, appendResponseHeader), three-phase request handling, AsyncLocalStorage context.
Popular docs & writing skills
grill-with-docs
mattpocock/skills
A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.
lark-markdown
open.feishu.cn
飞书 Markdown:查看、创建、上传、编辑和比较飞书中的原生 Markdown 文件。当用户要操作飞书 Markdown 文件,或比较其远端版本及本地草稿时使用。纯本地 Markdown 文件操作不触发本 skill。不负责将 Markdown 导入为飞书在线文档,也不负责文件搜索、权限、评论、移动、删除等云空间管理操作。
writing-beats
mattpocock/skills
Writing, exploit; assemble raw material into a journey of beats, grounding each term before a beat leans on it.
writing-shape
mattpocock/skills
Writing, exploit: shape raw material into an article, paragraph by paragraph.
writing-fragments
mattpocock/skills
Writing, explore: mine raw fragments, no structure yet.
writing-for-agents
mattpocock/skills
Writing documents for agents. Use when creating or editing skills, or modifying AGENTS.md or CLAUDE.md.