Skip to content

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 initialState from the loader rather than discovering it after mount.
  • Router APIs differ between major versions. useRouterSync takes the params and a setter, so it works with anything that has those two, but you supply the adapter.