Skip to content

taro-28/tanstack-table-search-params

Repository files navigation

TanStack Table Search Params

NPM Version NPM Downloads GitHub Repo stars Bundlephobia Minzipped size

React Hook for syncing TanStack Table state with URL search params.

tanstack-table-search-params.mp4

🚀 Quick Start

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:

🔍 How it works

The useTableSearchParams hook primarily does the following two things:

  • Decode query (query parameter state) and return it as the state for Tanstack Table.
  • Return a function like onChangeGlobalFilter that encodes state as a query parameter and performs replace (or push).

⚙️ Options

🏷️ Custom query param name

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}`,
  },
});

🔢 Custom default value

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]);

🪄 Custom encoder/decoder

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),
        })),
  },
});

⏱️ Debounce

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,
});

💬 Troubleshooting

Q. The page transitions every time the search params change

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 }),
});

Q. The value during IME conversion is set to search params

Create an input that supports IME conversion with a uncontrolled component.

Supported

List of supported TanStack table states

  • globalFilter
  • sorting
  • pagination
  • columnFilters
  • columnOrder
  • columnPinning
  • columnSizing
  • columnSizingInfo
  • columnVisibility
  • expanded
  • grouping
  • rowPinning
  • rowSelection

Roadmap

  • Support other table states
  • Disable specific state
  • Add onChangeXxxQuery option

TODO

  • Add examples for other routers
  • Add e2e tests

License

MIT