React Hook for syncing TanStack Table state with URL search params.
https://github.com/user-attachments/assets/1f1b4a65-fdec-4a80-a5d5-783642befaa3
First, install the package.
npm i tanstack-table-search-params
For example, if you are using Next.js (Pages Router), you can use the hook like this.
import { useReactTable } from "tanstack-table";
import { useRouter } from "next/router";
import { useTableSearchParams } from "tanstack-table-search-params";
const router = useRouter();
// Get state and onChanges
const stateAndOnChanges = useTableSearchParams({
query: router.query,
pathname: router.pathname,
replace: router.replace,
// or
push: router.push,
});
const table = useReactTable({
// Set state and onChanges
...stateAndOnChanges,
data,
columns,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
// ... other options
});
Here is the demo.
Of course, you can use it with other routers.
Please refer to the examples below:
The useTableSearchParams
hook primarily does the following two things:
- Decode
query
(query parameter state) and return it as thestate
for Tanstack Table. - Return a function like
onChangeGlobalFilter
that encodesstate
as a query parameter and performsreplace
(orpush
).
You can customize a query parameter name.
const stateAndOnChanges = useTableSearchParams(router, {
paramNames: {
// Customize query parameter name by passing a string
globalFilter: "userTable-globalFilter",
// Add prefix by passing a function
sorting: (defaultParamName) => `userTable-${defaultParamName}`,
},
});
You can customize the default value of a query parameter.
The "default value" is the value that is used as the state
when the query parameter is not present.
const stateAndOnChanges = useTableSearchParams(router, {
defaultValues: {
// Sort by name in descending order when query parameter is not present
sorting: [{ id: "name", desc: true }],
},
});
If you want to set initial values for query parameters, either transition with the query parameter or add the query parameter after the transition, depending on the router you are using.
// Transition with the query parameter
<Link href={{ pathname: "/users", query: { globalFilter: "foo" } }}>
Users
</Link>;
// Add the query parameter after the transition
useEffect(() => {
router.replace({ query: { globalFilter: "foo" } });
}, [router.replace]);
You can customize the encoder/decoder for the query parameter.
const stateAndOnChanges = useTableSearchParams(router, {
// Use JSON.stringify/JSON.parse for encoding/decoding
encoders: {
// foo -> { "globalFilter": "foo" }
globalFilter: (globalFilter) => ({
globalFilter: JSON.stringify(globalFilter),
}),
},
decoders: {
// { "globalFilter": "foo" } -> foo
globalFilter: (query) =>
query["globalFilter"]
? JSON.parse(query["globalFilter"] as string)
: (query["globalFilter"] ?? ""),
},
});
// ...
const stateAndOnChanges = useTableSearchParams(router, {
// Encoders/decoders with different query parameter names can also be used.
encoders: {
// [{ id: "name", desc: true }] -> { "userTable-sorting": "[{ \"id\": \"name\", \"desc\": true }]" }
sorting: (sorting) => ({
"userTable-sorting": JSON.stringify(sorting),
}),
},
decoders: {
// { "userTable-sorting": "[{ \"id\": \"name\", \"desc\": true }]" } -> [{ id: "name", desc: true }]
sorting: (query) =>
query["userTable-sorting"]
? JSON.parse(query["userTable-sorting"] as string)
: query["userTable-sorting"],
},
});
// ...
const stateAndOnChanges = useTableSearchParams(router, {
// Encoders/decoders with different numbers of query parameters can also be used.
encoders: {
// [{ id: "name", value: "foo" }] -> { "columnFilters.name": "\"foo\"" }
columnFilters: (columnFilters) =>
Object.fromEntries(
columnFilters.map(({ id, value }) => [
`columnFilters.${id}`,
JSON.stringify(value),
]),
),
},
decoders: {
// { "columnFilters.name": "\"foo\"" } -> [{ id: "name", value: "foo" }]
columnFilters: (query) =>
Object.entries(query)
.filter(([key]) => key.startsWith("columnFilters."))
.map(([key, value]) => ({
id: key.replace("columnFilters.", ""),
value: JSON.parse(value as string),
})),
},
});
You can debounce the reflection of state changes in the query parameters.
const stateAndOnChanges = useTableSearchParams(router, {
debounceMilliseconds: {
// Debounce globalFilter by 500 milliseconds
globalFilter: 500,
},
});
Also, you can debounce all query parameters at once.
const stateAndOnChanges = useTableSearchParams(router, {
debounceMilliseconds: 500,
});
If you are using Next.js (Pages Router), you can prevent page transitions by using the shallow
option.
const router = useRouter();
const stateAndOnChanges = useTableSearchParams({
...router,
replace: (query) => router.replace(query, undefined, { shallow: true }),
});
Create an input that supports IME conversion with a uncontrolled component.
List of supported TanStack table states
- [x] globalFilter
- [x] sorting
- [x] pagination
- [x] columnFilters
- [ ] columnOrder
- [ ] columnPinning
- [ ] columnSizing
- [ ] columnSizingInfo
- [ ] columnVisibility
- [ ] expanded
- [ ] grouping
- [ ] rowPinning
- [ ] rowSelection
- [ ] Support other table states
- [ ] Disable specific state
- [ ] Add
onChangeXxxQuery
option
- [ ] Add examples for other routers
- [ ] Add e2e tests
MIT