nuqs-table-url-state
nuqs-table-url-state documents @cogs/nuqs's createTableUrlState factory, which wires shareable, bookmarkable pagination, filters, sort, and search into a TanStack-Table-backed list view from a single config object per screen — instead of hand-rolling a use*Filters.ts hook for every table. It builds directly on the generic nuqs foundation (parsers, useQueryState, hydration) covered by the sibling nuqs-url-state skill, and is scoped specifically to the table/filter/pagination layer on top of it.
What it does
createTableUrlState takes one config object — filters (built with buildFilterParsers), sort, search, and pageSize — and returns a useTableUrlState hook plus buildUrl/searchParamsAPI/serializer for building links outside React. The hook exposes pagination, filters, sorting, searchQuery, and a derived isAnyFilterActive boolean, alongside setters (setFilters, setSorting, setSearchQuery, setPagination) and both a combined reset (resetFiltersAndSearch) and split resets (resetFilters, resetSearch).
Two structural rules are baked in rather than left to each screen to remember:
- Every filter, sort, or search change resets pagination to page 0.
setFilters,setSorting, andsetSearchQueryall resetpagination.pageas part of the same state update — not optional or configurable. - Page indices convert automatically.
pagination.pageis 0-indexed internally (matching TanStack Table'sPaginationStateexactly, so there's no adapter layer), while the URL is 1-indexed for human-friendly links. ThepageIndexParserStrictparser handles the+1/-1conversion at the URL parse/serialize boundary — you never write it yourself.
It also ships buildFilterParsers (turns a list of filter-field descriptors into faceted-array or plain-string parsers automatically) and getColumnsSortParser (encodes ColumnSort[] as col-dir,col-dir URL tokens, with optional validation of sort tokens against a sample row shape), plus a stateful mock under @cogs/nuqs/testing for tests that exercise this wiring without hitting real browser URL APIs.
Why it was created
The skill's own metadata records a concrete motivating survey rather than a general design preference: a review of envmgr-ui, a table-heavy Next.js app, found ~20 near-identical use*Filters.ts hooks — useBestPracticesFilters.ts, usePodRestartFilters.ts, useHealthCheckFilters.ts, useDeploymentHistoryFilters.ts, useDnsHealthFilters.ts, and roughly fifteen more — each implementing the same ~150-line template (useQueryState per scalar field, batched useQueryStates for pagination and filters, { shallow: true } re-applied by hand, and four callbacks that all called setPage(0, ...)) with zero behavioral variance between them. The commit history confirms this: @cogs/nuqs was added in a single commit, feat(nuqs): add @cogs/nuqs, ported from envmgr-ui's table URL-state patterns, which describes extracting the page-index conversion, the always-reset factory, the parser builders, and the stateful test mock directly from that survey. envmgr-ui itself was not modified — this package is a clean-room extraction of the pattern, not a live migration.
The always-reset-page-to-0 rule specifically is justified the same way: it was found with zero exceptions across the ~20 surveyed hooks, so the factory makes it structural (impossible to forget) rather than something each new hook has to re-implement correctly.
There is no dedicated founding dossier narrating why this table factory was built, so the provenance above comes from the skill's own metadata.json abstract and its two git commits, not a retrospective writeup.
How it works
- Config vs. hook-call options are deliberately split.
filters,sort,search, andpageSizeare evaluated once per screen increateTableUrlState, outside any component.startTransition, by contrast, is auseTableUrlState(options)argument, not a factory-config field — because it can only come fromuseTransition()inside a component, which doesn't exist yet at factory-definition time. When provided, it's threaded uniformly into every parser'swithOptions()internally, resolving a divergence where roughly half the surveyed hooks appliedstartTransitionper-field and half omitted it for no discernible reason. filtersonly accepts array-valued (faceted) parsers.buildFilterParsersalso supports plain-string parsers for free-text fields, butcreateTableUrlStateitself only takes the faceted, array-valued ones — every hook in the original survey routed its filter-reset cascade through array-valued filters only. A standalone text filter should be wired as its ownuseQueryStatecall per the basenuqs-url-stateskill, not forced through this config.- Both reset shapes are always exposed, not chosen. Most surveyed hooks only had one
resetFiltersAndSearch; one (useDnsHealthFilters.ts) diverged and exposed splitresetFilters/resetSearchfor a "clear search but keep filters" affordance. Rather than picking one shape, the factory exposes both, and each screen picks whichever matches its actual UI. - The test mock round-trips real parse/serialize logic, not raw values.
@cogs/nuqs/testingre-exports the real parsers (parseAsString,pageIndexParserStrict, etc.) verbatim fromnuqs/server— they're pure functions — and only fakesuseQueryState,useQueryStates, andNuqsAdapter. That matters specifically forpageIndexParserStrict, where the internal (0-indexed) state and serialized URL (1-indexed) are deliberately different values: a mock that just stores whatever it's given would let a test assert an incorrect index relationship and still pass. The mock also respectsclearOnDefault, so a param correctly disappears from the URL when reset to its default instead of lingering as an explicit value. - No import-time side effects. In the surveyed app, two diagnostics tables forked a local copy of the shared page-index parser specifically to dodge an import graph that triggered unrelated navigation-config initialization and broke test isolation.
@cogs/nuqsmodules do nothing at import time beyond defining parsers/constants/hooks, so that "local copy to avoid coupling" workaround shouldn't be necessary when adopting this package.
When to use it / when not to
Reach for createTableUrlState when building or reviewing a TanStack-Table-backed list view that needs URL-shareable pagination plus any combination of filters, sort, or search — especially if you're about to write a new use*Filters.ts-style hook, or you're debugging a table where changing a filter doesn't reset the page back to the first one.
It's the wrong tool when:
- You need a single standalone query param outside a table context — use the generic
nuqs-url-stateskill's parser/useQueryStateguidance instead. - You need a plain-text filter field alongside table state — wire it as its own
useQueryStatecall rather than forcing it throughcreateTableUrlState'sfiltersconfig, which only accepts faceted array parsers. - You're mocking nuqs in tests without going through
@cogs/nuqs/testing— a hand-rolled no-op mock can hide real broken URL wiring, since assertions that don't depend on the setter's effect will pass even if the component never calls it correctly.