TanStack Router Adapter
Sync data table state with TanStack Router search params.
The TanStack Router adapter uses TanStack Router's type-safe search params to persist filter state in the URL.
Installation
npx shadcn@latest add https://ada-table.vercel.app/r/tanstack-router-adapter.jsonThis installs the adapter files automatically. No additional packages needed beyond @tanstack/react-router.
Setup
1. Define the search schema on your route
Add the data table search schema to your route definition using validateSearch:
import { createFileRoute } from "@tanstack/react-router";
import { dataTableSearchSchema } from "@/components/data-table/integrations/tanstack-router";
export const Route = createFileRoute("/tasks")({
validateSearch: dataTableSearchSchema,
loaderDeps: ({ search }) => search,
loader: async ({ deps: search }) => {
// Use search.filters, search.sort, search.page, search.perPage
// to query your database
},
});2. Use the adapter in your component
import { useTanstackRouterSearchAdapter } from "@/components/data-table/integrations/tanstack-router";
import { useDataTable } from "@/components/data-table/hooks/use-data-table";
function TasksPage() {
const adapter = useTanstackRouterSearchAdapter();
const { data, pageCount } = Route.useLoaderData();
const { table, filterColumns, filters, joinOperator, actions } = useDataTable({
data,
columns,
columnsConfig,
strategy: "server",
adapter,
pageCount,
});
// Render your table...
}How It Works
The adapter uses useSearch({ strict: false }) to read the current search params from any route in the tree. This means your data table component doesn't need to know which specific route it's rendered on.
When filters change, the adapter calls navigate({ search: nextState, replace: true }) to update the URL without adding history entries.
Search Schema
The dataTableSearchSchema is a Zod schema that validates and provides defaults for all search state fields:
import { dataTableSearchSchema, defaultDataTableSearch } from "@/components/data-table/integrations/tanstack-router";
// Schema shape:
// {
// filters: z.array(filterModelSchema).default([]),
// joinOperator: z.enum(["and", "or"]).default("and"),
// page: z.number().int().positive().default(1),
// perPage: z.number().int().positive().default(10),
// sort: z.array(z.object({ id: z.string(), desc: z.boolean() })).default([]),
// }
// Defaults:
// {
// filters: [],
// joinOperator: "and",
// page: 1,
// perPage: 10,
// sort: [],
// }Loader Integration
TanStack Router's loaderDeps + loader pattern works well with server-side filtering:
export const Route = createFileRoute("/tasks")({
validateSearch: dataTableSearchSchema,
loaderDeps: ({ search }) => search,
loader: async ({ deps: search, context: { db } }) => {
const conditions = search.filters
.map((f) => buildFilterCondition(tasksTable, f))
.filter(Boolean);
const where = search.joinOperator === "and"
? and(...conditions)
: or(...conditions);
const [data, countResult] = await Promise.all([
db.select().from(tasksTable)
.where(where)
.orderBy(/* ... */)
.limit(search.perPage)
.offset((search.page - 1) * search.perPage),
db.select({ count: count() }).from(tasksTable).where(where),
]);
return {
data,
pageCount: Math.ceil(countResult[0].count / search.perPage),
};
},
});