iTranslated by AI
I built a library to sync TanStack Table state with URL parameters
Tanstack Table is a high-performance headless UI library for creating tables that can be used with various libraries/frameworks such as React, Vue, and Solid.
This time, I've created a library called tanstack-table-search-params that allows you to synchronize states like search and sorting with URL parameters when using TanStack Table with React, so I'd like to introduce it.
TanStack Table
First, I will briefly introduce how to use TanStack Table in React.
In TanStack Table, you create a table by passing the data to be displayed and the column definitions to a hook called useReactTable. You then access table states and handlers from the return value to build your table.
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
type User = { id: string; name: string };
// Data to be displayed in the table
const data: User[] = [
{ id: "1", name: "John" },
{ id: "2", name: "Sara" },
];
// Column definitions
const columnHelper = createColumnHelper<User>();
const columns = [
columnHelper.accessor("id", {}),
columnHelper.accessor("name", {}),
];
export default function UserTable() {
// Create the table
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
return (
<div>
<table>
<thead>
<tr>
{table.getFlatHeaders().map((header) => (
<th key={header.id}>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</th>
))}
</tr>
</thead>
<tbody>
{/* Access and display the table state */}
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getAllCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}

Furthermore, here is the same table with search implemented:
import {
// ...
+ getFilteredRowModel,
} from "@tanstack/react-table";
// ...
export default function UserTable() {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
});
return (
<div>
+ <input
+ placeholder="Search..."
+ value={table.getState().globalFilter}
+ onChange={(e) => table.setGlobalFilter(e.target.value)}
+ />
<table>

In this context, getCoreRowModel and getFilteredRowModel are functions called RowModel that implement various features of the table. In TanStack Table, you can add functionality such as search and sorting by using built-in RowModels like getFilteredRowModel (importable from @tanstack/react-table) or by using your own custom RowModels.
tanstack-table-search-params
Next, let's use the tanstack-table-search-params library to synchronize the search state with URL parameters.
Below is an example using the Next.js Pages Router.
With tanstack-table-search-params, you can synchronize the TanStack Table state with URL parameters simply by:
- Importing
useTableSearchParamsand calling it by passing the return object from Next.js'suseRouteras an argument. - Passing the return value of
useTableSearchParamsinto theuseReactTablearguments.
+ import { useRouter } from "next/router";
+ import { useTableSearchParams } from "tanstack-table-search-params";
// ...
export default function UserTable() {
+ const router = useRouter();
+ const stateAndOnChanges = useTableSearchParams(router);
const table = useReactTable({
+ ...stateAndOnChanges,
// ...

How it works: Synchronizing with URL parameters
To explain how tanstack-table-search-params synchronizes the TanStack Table state with URL parameters, let's modify the code above—which passed the entire router and stateAndOnChanges objects—into a version that focuses on the necessary properties.
export default function UserTable() {
const { query, pathname, replace} = useRouter();
const stateAndOnChanges = useTableSearchParams({
query: router.query,
pathname: router.pathname,
replace: router.replace,
});
const table = useReactTable({
state: {
globalFilter: stateAndOnChanges.state.globalFilter,
},
onGlobalFilterChange: stateAndOnChanges.onGlobalFilterChange,
// ...
Passing state.globalFilter and onGlobalFilterChange to useReactTable here is the standard way to manage TanStack Table's state externally.
As shown in the code, useTableSearchParams is a hook that takes:
-
query: The React state of the URL parameters -
pathname: The current URL path -
replace(orpush)
And returns:
state.globalFilteronGlobalFilterChange
As you might imagine from looking at the arguments and return values, what this hook does is very simple.
First, state is created by converting (decoding) query into TanStack Table's TableState type.
Next, onGlobalFilterChange simply returns a function that converts (encodes) each TanStack Table state into URL parameters and executes replace.
A key characteristic of both the state and onGlobalFilterChange creation is that they are simple transformations (object to object, function to function) and do not maintain any internal state. (*Except for the debounce feature mentioned later, which uses an internal state for debouncing.)
Supported TanStack Table states
Currently (as of 2024/11/20), the following four TanStack Table states are supported, with plans to add more in the future.
- globalFilter: Global row search
- sorting: Sorting
- pagination: Pagination
- columnFilters: Column-specific search
Supported routers
As introduced in How it works: Synchronizing with URL parameters, tanstack-table-search-params simply converts URL parameter React state into TanStack Table state. Therefore, it can (probably) be used with any router that can retrieve URL parameters as React state.
We have examples prepared for the following three, so please take a look if you are interested.
Other configuration options
With tanstack-table-search-params, you can customize URL parameter names, encoding formats, and more.
URL parameter names
By default, URL parameter names use the same names as the TableState type, such as globalFilter and sorting.
To change the URL parameter names, you can specify them via paramNames in the second argument of useTableSearchParams.
const stateAndOnChanges = useTableSearchParams(router, {
paramNames: {
// Change the URL parameter name
globalFilter: "search",
},
});
You can also pass a function if you want to add a prefix or suffix.
const stateAndOnChanges = useTableSearchParams(router, {
paramNames: {
// Add a prefix to the URL parameter name
globalFilter: (defaultName) => `userTable-${defaultName}`,
},
});
Encoding format
By default, the encoding format for URL parameter values is a simple format as follows:
| State value | URL parameter example |
|---|---|
| Global row search | ?globalFilter=John |
| Sorting | ?sorting=name.desc |
| Pagination | ?pageIndex=2&pageSize=20 |
To change the encoding format, you can specify it using encoders and decoders in the second argument of useTableSearchParams.
const stateAndOnChanges = useTableSearchParams(router, {
encoders: {
// Change the encoding format to JSON.stringify
globalFilter: (globalFilter) => ({
globalFilter: JSON.stringify(globalFilter),
}),
},
decoders: {
globalFilter: (query) =>
query["globalFilter"]
? JSON.parse(query["globalFilter"])
: (query["globalFilter"] ?? ""),
},
});
It is also possible to change both the encoding format and the parameter name at the same time.
const stateAndOnChanges = useTableSearchParams(router, {
encoders: {
sorting: (sorting) => ({
// Change the encoding format to JSON.stringify and the parameter name to my-sorting
"my-sorting": JSON.stringify(sorting),
}),
},
decoders: {
sorting: (query) =>
query["my-sorting"]
? JSON.parse(query["my-sorting"])
: query["my-sorting"],
},
});
Debounce
You can also debounce the synchronization with URL parameters.
const stateAndOnChanges = useTableSearchParams(router, {
debounceMilliseconds: 500,
});
It is also possible to debounce only specific TanStack Table states.
const stateAndOnChanges = useTableSearchParams(router, {
debounceMilliseconds: {
// Debounce the synchronization of globalFilter with URL parameters by 0.5 seconds
globalFilter: 500,
},
});
Default values
You can specify the default TanStack Table state values for when URL parameters are not present.
For example, in the case of sorting, if no default values are specifically set, the state.sorting values (sorting conditions) and their corresponding URL parameters are as follows:
state.sorting value |
URL parameter |
|---|---|
[] |
None |
[{ id: "createdAt", desc: true }] |
?sorting=createdAt.desc |
[{ id: "createdAt", desc: false }] |
?sorting=createdAt.asc |
*createdAt is the column name.
In contrast, when the default sort order is set to descending as shown below:
const stateAndOnChanges = useTableSearchParams(router, {
defaultValues: {
sorting: [{ id: "createdAt", desc: true }],
},
});
The state.sorting values and URL parameters will be as follows:
state.sorting value |
URL parameter |
|---|---|
[] |
?sorting=none |
[{ id: "createdAt", desc: true }] |
None |
[{ id: "createdAt", desc: false }] |
?sorting=createdAt.asc |
Conclusion
I originally created this library because I was using TanStack Table at work and wanted to synchronize it with URL parameters more easily.
(I have already started using it at work, and it is also briefly mentioned on our service's announcement page.)
Now that I have implemented all the features I personally wanted, I plan to improve its completeness by increasing the number of supported TanStack Table states and expanding the customizable aspects.
As a side note, this was my first time publishing a package I created on my own to npm, and it was great to learn about the npm publishing process and the convenience of tsup.
Please feel free to use it! (I would be very happy if you could give it a star too!)
Discussion