React Router
Put the table's state in the URL through the router's own history, so filters are navigations and the back button works.
Basics
The table can write its state to the URL on its own, but in a routed application that means
changing the address bar behind the router's back. useRouterSync hands the reading and writing
to the router instead:
import { useSearchParams } from 'react-router';
import { DataTable, useDataTable, useRouterSync } from '@kingtech/react-tablekit';
function People() {
const [searchParams, setSearchParams] = useSearchParams();
const table = useDataTable({ data: rows, columns, getRowId: (r) => r.id });
useRouterSync(table, {
getSearch: () => searchParams.toString(),
setSearch: (search, mode) => setSearchParams(search, { replace: mode === 'replace' }),
});
return <DataTable table={table} aria-label="People" />;
}
Now a filter change is a navigation the router knows about: the back button undoes it, a link reproduces it, and a loader can read it.
Replace, not push
Use replace: true for anything that changes while the reader types or clicks around: search,
filters, sorting. Otherwise the back button walks through every keystroke's worth of history.
Pushing is reasonable for page changes, where going back to the previous page is what a reader
expects.
Reading the state in a loader
Because the state is in the search params, a route loader can read it and fetch server-side:
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
return api.people({
page: Number(url.searchParams.get('tk.page') ?? 0),
q: url.searchParams.get('tk.q') ?? '',
});
}
Pair that with dataMode="server" and the table renders what the loader returned.
What belongs in storage instead
Visible columns, their order, their widths and the density are the reader's own preferences, not
part of the address. Keep those in syncState.storage so links stay short and shareable; see
Persistence.
Limitations
- The parameter prefix is shared with the page.
tk.keeps the table's parameters out of the way, but two tables on one route need different prefixes or they will fight. - Loaders run before hydration, so a table whose state is loader-driven should receive
initialStatefrom the loader rather than discovering it after mount. - Router APIs differ between major versions.
useRouterSynctakes the params and a setter, so it works with anything that has those two, but you supply the adapter.