npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sovgut/datagrid-react-router

v3.0.1

Published

Keeps @sovgut/datagrid table state in the URL: pagination, sorting, per-column filters and selection round-trip through React Router's search params, so every table view is a shareable link.

Readme

Contents

What this is

One hook. It implements DataGridReducer from @sovgut/datagrid on top of React Router's useSearchParams, so the grid reads and writes the query string instead of its own store.

It is the whole state layer for a table whose state should be in the address bar: back and forward work, a reload keeps the view, and a link carries the filters with it.

It is not a grid, and it does not render anything. The core owns the table logic and your own components own the markup; this package only decides where the state lives.

Installation

npm install @sovgut/datagrid-react-router

Three peer dependencies, all of which you already have if you are using the grid with React Router:

| Peer | Range | | :--- | :--- | | @sovgut/datagrid | ^5.0.0 | | react | ^19.0.0 | | react-router | ^7.0.0 \|\| ^8.0.0 |

The core is a peer rather than a bundled dependency on purpose: the grid keeps module-level defaults that it documents as single values shared by every table on the page, and a second copy of the core would quietly break that.

Quick start

import { DataGrid, type DataGridColumn, type DataGridRow } from "@sovgut/datagrid";
import { useSharedDataGrid } from "@sovgut/datagrid-react-router";
import { useSearchParams } from "react-router";
import { useMemo } from "react";

interface Order extends DataGridRow {
  id: number;
  status: string;
}

export function Orders() {
  const columns = useMemo<DataGridColumn<Order>[]>(
    () => [
      { key: "id", label: "ID" },
      { key: "status", label: "Status", sortable: true },
      { key: "bank", label: "Bank", multiple: true },
    ],
    []
  );

  // The whole integration: hand the hook the tuple and the columns, hand the
  // result to `store`.
  const store = useSharedDataGrid<Order>(useSearchParams(), columns);
  const { data } = useOrders(store);

  return (
    <DataGrid store={store} columns={columns} rows={data.items} size={data.total}>
      <OrdersTable />
    </DataGrid>
  );
}

store is also the query: read store.page, store.sort and store.filter directly when you build the request. There is no second source of truth to keep in sync.

What lands in the URL

| Parameter | Written by | Shape | | :--- | :--- | :--- | | page | setPagination, setState | a positive integer | | limit | setPagination, setState | a positive integer | | sort | setSorting, setState | a column key; removed when unsorted | | order | setSorting, setState | asc or desc; removed when unsorted | | selected | setSelected, setState | repeated once per selected id | | one per column key | setFilter, setState | the filter value, repeated for an array |

A column key is a query parameter name. That makes the five names above reserved, and the hook throws on mount if a column claims one. It would otherwise fail silently and destructively: writing the filter would overwrite the grid's own parameter, and clearing the filter would refuse to remove it.

It also makes a column key part of your public URLs, so renaming one breaks links that are already out there.

Reading it back

A URL is typed by nobody, so everything is validated on the way in:

  • Filter values come back as strings. A column marked multiple gets a string[] from every repetition of its parameter; every other column gets a single string. If your API wants numbers, convert at the call site.
  • page and limit fall back to the package defaults unless the parameter is a positive integer. ?page=abc is treated as no page at all rather than being passed on as NaN.
  • order must be exactly asc or desc. Anything else is read as unsorted, because those are the only two values this hook ever writes and the grid's type promises nothing wider.
  • Unknown parameters are left alone. Only keys that match a column are collected into filter, so the table can share a URL with the rest of your app.

deriveState runs here, on purpose

The hook applies every column's filterConfig.deriveState to the state it read from the URL, before returning it.

That is deliberate and it matters: a column that enforces an implicit filter (only your own records, only this organization) has to have applied it by the time you build the first request. Left to the grid, the first fetch would go out without it and its result would be thrown away one render later.

The state handed to deriveState is a copy: the state object, its filter and its selected are fresh, and values nested inside a filter entry are shared by reference. Assign and delete filter entries freely; replace nested values rather than editing them in place.

The setters

All five write to the URL and nothing else. There is no local state behind them.

  • setFilter replaces the filter wholesale. Every parameter that is not one of the five reserved names is cleared first, so a key the new filter does not mention disappears. To clear one filter, pass the rest.
  • A null or undefined filter value is skipped, at the entry level and inside an array, rather than being written as the string "undefined".
  • setSorting accepts undefined for either argument and treats it like null, removing the parameter.
  • None of them replace the history entry. Every change is a new entry, so the browser's back button steps through the user's filtering. If you need something else, that is a change to this package rather than an option.
  • Calling store.setFilter directly does not reset the page. The grid's own resetPageOnQueryChange applies to the grid's setters; reach into the store and you are talking to the URL, not to the grid.

Performance notes

Memoize columns. The hook derives its state from them, so a fresh array on every render recomputes that state on every render and hands the grid a new store object each time. useMemo on the column array is the whole fix.

The returned object is memoized here, which the core's documentation requires of an external store: an unstable one is the one known way to turn the grid's synchronization pass into an update loop.

API reference

useSharedDataGrid(searchParams, columns)

| Parameter | Type | Description | | :--- | :--- | :--- | | searchParams | [URLSearchParams, SetURLSearchParams] | The tuple returned by useSearchParams(). Pass it straight through. | | columns | DataGridColumn<TData>[] | The same definitions the grid gets. Their keys name the query parameters and their deriveState runs here. |

Returns a DataGridReducer: the state fields below plus setPagination, setSorting, setFilter, setSelected and setState.

| Field | Type | Source | | :--- | :--- | :--- | | page | number | ?page, else 1 | | limit | number | ?limit, else 10 | | sort | string \| null | ?sort | | order | "asc" \| "desc" \| null | ?order | | filter | Record<string, any> | one parameter per column key | | selected | string[] | every ?selected |

Every field is synced with the URL, selection included.

Contributing

Issues and pull requests are welcome. Before opening one:

npm ci
npm run check      # Biome, rewrites files
npm run typecheck
npm test           # runs coverage, which is gated at 100%
npm run build

License

MIT