gridcn

URL state

Sync sort, filter, search, and pagination to the URL via nuqs.

data-grid-url-state syncs the sort, filter, search, and pagination state of the grid with the URL using nuqs.

pnpm dlx shadcn@latest add @gridcn/data-grid-url-state

Usage

Mount <DataGridUrlState> once inside <DataGridProvider>, under a NuqsAdapter for your framework somewhere above it in the tree:

"use client";

import { NuqsAdapter } from "nuqs/adapters/next/app";
import { DataGridUrlState } from "@/components/data-grid-url-state/data-grid-url-state";
import { DataGridToolbar, DataGridSearch, DataGridFilterMenu } from "@/components/data-grid-toolbar/data-grid-toolbar";

function OrdersGrid() {
  return (
    <NuqsAdapter>
      <DataGridProvider {...grid} columns={columns}>
        <DataGridUrlState prefix="orders" />
        <DataGridToolbar>
          <DataGridSearch />
          <DataGridFilterMenu />
        </DataGridToolbar>
        <DataGridRoot>
          <DataGridHeader />
          <DataGridBody />
        </DataGridRoot>
      </DataGridProvider>
    </NuqsAdapter>
  );
}

nuqs/adapters/next/app is the Next.js App Router adapter shown above. Swap it for your framework's own adapter, per the nuqs docs (Pages Router, Remix, React Router, or plain React).

NuqsAdapter must not be nested

If your app already renders a NuqsAdapter once near the root, the common case, drop the inner one shown above. DataGridUrlState only needs one NuqsAdapter somewhere above it in the tree.

Suspense required when statically prerendering

Because the adapter reads useSearchParams(), wrap the grid in <Suspense> if you statically prerender the page. Next.js requires this.

prefix

This namespaces the sort, filter, join, and q URL keys, so multiple grids can coexist on one page without colliding:

<DataGridUrlState prefix="orders" />
// -> ?orders_sort=...&orders_filter=...&orders_join=...&orders_q=...

The filter menu's And/Or toggle writes the join param (omitted while the operator is the default). It has its own key instead of folding into filter, so a URL generated before the toggle existed still parses identically.

Behavior

  • Reload the page or share a link, and the view comes back exactly as it was.
  • URL params apply to the store exactly once, on mount. After that, the store is the source of truth and drives the URL, not the other way around.
  • Sort, filter, and join writes are immediate, since they are menu-driven, not keystroke-driven. Search writes are debounced (300ms), so typing does not spam history.replaceState.
  • Every URL write uses history: "replace", never push, so typing in search or changing a filter does not pollute the browser's back and forward history.
  • The core stays router-agnostic. data-grid-url-state is the only piece that knows about a router, and it is entirely optional.

A URL-applied sort is invisible in the default config

?sort=name:asc reorders the rows, but the built-in sort indicator only renders in headerClickBehavior="sort" mode, which is not the default. So in the default config a deep-linked sort changes the row order with no visible arrow and no built-in UI to clear it (the sort-list toolbar is a separate opt-in add-on). The sync works; the affordance is just missing. Make it visible and clearable:

<DataGridProvider headerClickBehavior="sort" {...grid} columns={columns}>

or install data-grid-sort-list for a toolbar sort list.

Column layout is not synced to the URL

Column widths, order, pins, and hidden state are not part of this add-on. Use defaultColumnLayout and onColumnLayoutChange instead. See Columns: persisting a user's layout.

Pagination

useDataGridUrlPagination syncs page and pageSize with the URL, separately from <DataGridUrlState> — pagination has no store slot, so this is a standalone hook rather than something <DataGridUrlState> mounts for you:

"use client";

import { useDataGridUrlPagination } from "@/components/data-grid-url-state/data-grid-url-state";
import { useDataGridPagination, DataGridPaginationBar } from "@/components/data-grid-pagination/data-grid-pagination";

function OrdersGrid({ rows, total }: { rows: Order[]; total: number }) {
  const url = useDataGridUrlPagination();
  const pager = useDataGridPagination({ total, ...url });

  return (
    <>
      <DataGrid data={rows} columns={columns} getRowId={(row) => row.id} />
      <DataGridPaginationBar {...pager.controls} />
    </>
  );
}

page (1-based) is omitted from the URL at 1; pageSize is omitted when it equals the configured default. Both writes use history: "replace", same as this add-on's other params. Options: prefix (namespaces page/pageSize the same way as sort/filter/q), defaultPageSize (default 25), and pageSizeOptions (an out-of-range URL pageSize falls back to the default instead of being trusted). See Pagination for the full composition, including why server mode (not client mode) is the fit.

On this page