Refine URL collection state: single useURLCollectionVariables hook + typed filter coercion#335
Conversation
…typed filter values Proposed refinements on top of improve-use-url-collection-state. Interface: collapse the URL-state common path into one hook. useURLCollectionState() only ever wrapped react-router's useSearchParams, so it added no flexibility over a fused hook while forcing a two-hook dance. Replace it with useURLCollectionVariables(options) (read + write URL state in one call). Keep useCollectionVariables (router-free primitive) and the pure withURLCollectionState(options, binding) decorator (custom-binding escape hatch). Fix: parseCollectionSearchParams now coerces decoded filter values to the field's declared metadata type (number/boolean, incl. in/nin arrays and between bounds). Previously a URL-restored numeric filter such as f.price:gt=130 came back as the string "130", contradicting TypedCollectionVariables and silently breaking type-aware filtering. The untyped overload (no metadata) is unchanged. Update the nextjs data-table-demo to the new hook, refresh the changeset, and add tests for coercion and the numeric write -> parse round-trip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…iables Runnable DataTable example with a mock remote query (filters, sort, cursor pagination, multi-select) wired through useURLCollectionVariables, so state is persisted to and hydrated from the URL. Doubles as the repro for the typed filter-value coercion fix (e.g. ?f.price:gt=130 now hydrates correctly). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
I originally preferred the That said, after thinking about it more, I agree that The key point for me is that the important abstraction is still I also don’t think keeping So I’m 👍 on |
ff8c086
into
improve-use-url-collection-state
* improve: simplify and harden useUrlCollectionState
- Remove params from write effect deps using function updater to eliminate
feedback loop (setParams → params change → effect re-runs)
- Fix filter value encoding: use JSON for arrays to correctly handle values
containing commas (e.g. "Smith, John")
- Fix hydration race condition: introduce SyncPhase lifecycle (pending →
hydrated → ready) to prevent writing stale defaults to URL before
hydrated state propagates
- Simplify decodeFilterValue: remove startsWith check, rely on
JSON.parse + Array.isArray for correctness
- Replace Array.from(next.keys()) with spread syntax
* fix: round-trip object-valued filters in useUrlCollectionState
decodeFilterValue only parsed JSON arrays back, so object-valued filters
(the `between` operator's { min, max } shape) round-tripped to a raw
string on reload and silently broke. Decode objects too; primitives like
"5"/"true" still fall through to strings, preserving string-vs-numeric
filter semantics.
Also harden the write-effect bail-out: compare on a sorted, JSON-encoded
snapshot (stableQueryString) rather than `.toString()`, which is
sensitive to filter key-insertion order and to `&`/`=` inside values.
Folds in fixes already running in the Denim Tears IMS copy of this hook.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(core): add withURLState helper for collection variables
* refactor(core): move collection URL state helpers to lib
* Refine URL collection state API
* Remove withURLState alias
* Add useURLCollectionState hook
* Make useURLCollectionState return a decorator
* Align collection state params callbacks
* Refine URL collection state: single `useURLCollectionVariables` hook + typed filter coercion (#335)
* refactor(collection): single useURLCollectionVariables hook + coerce typed filter values
Proposed refinements on top of improve-use-url-collection-state.
Interface: collapse the URL-state common path into one hook.
useURLCollectionState() only ever wrapped react-router's useSearchParams,
so it added no flexibility over a fused hook while forcing a two-hook dance.
Replace it with useURLCollectionVariables(options) (read + write URL state in
one call). Keep useCollectionVariables (router-free primitive) and the pure
withURLCollectionState(options, binding) decorator (custom-binding escape hatch).
Fix: parseCollectionSearchParams now coerces decoded filter values to the
field's declared metadata type (number/boolean, incl. in/nin arrays and between
bounds). Previously a URL-restored numeric filter such as f.price:gt=130 came
back as the string "130", contradicting TypedCollectionVariables and silently
breaking type-aware filtering. The untyped overload (no metadata) is unchanged.
Update the nextjs data-table-demo to the new hook, refresh the changeset, and
add tests for coercion and the numeric write -> parse round-trip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(example): add vite-app Products page demoing useURLCollectionVariables
Runnable DataTable example with a mock remote query (filters, sort, cursor
pagination, multi-select) wired through useURLCollectionVariables, so state is
persisted to and hydrated from the URL. Doubles as the repro for the typed
filter-value coercion fix (e.g. ?f.price:gt=130 now hydrates correctly).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: interacsean <seanhasselback@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Proposed refinements on top of
improve-use-url-collection-state(#293), in two parts: a hook-interface change and a bugfix to the URL-state code on that branch. Scoped to the library + the one consumer update the interface change forces — no unrelated demo/scaffolding.1. Interface — collapse the common path into one hook
The base branch exposes URL persistence as a decorator returned by a hook, used like:
useURLCollectionState()only ever wraps react-router'suseSearchParams, so it provides no flexibility a single fused hook wouldn't — while forcing every call site through a two-hook dance and an awkward name collision (const withURLCollectionState = useURLCollectionState()shadows the exportedwithURLCollectionState).This collapses the 99% path to one call:
Public surface becomes three exports, each with a distinct job:
useCollectionVariables— router-free primitive (unchanged)useURLCollectionVariables(new) — URL persistence in one callwithURLCollectionState(options, binding)— pure decorator, kept as the escape hatch for a non-react-router binding / compositionRemoved
useURLCollectionState(redundant with the fused hook; unreleased, so no external consumers).2. Bugfix — typed filter values lost their type on URL round-trip
decodeFilterValueintentionally returns numeric/boolean values as strings (on its own it can't know the intended type). ButparseCollectionSearchParams— the metadata-aware overload — never restored the type, so a deep-linkedf.price:gt=130came back as the string"130". That contradictsTypedCollectionVariables(which declaresquery.price.gtasnumber) and silently breaks any type-aware filtering — a real typed GraphQLFloat/Intinput would also reject a string.Fix: when table metadata is present, coerce decoded filter values to the field's declared type (number/boolean), descending into
in/ninarrays andbetween{ min, max }bounds. The untyped overload (no metadata) is unchanged — values stay strings.Tests
inarrays) using metadatagtfilter through write → parse as anumberuseURLCollectionVariablesshapeVerification
packages/core: lint clean, format clean, 27 tests pass,tscclean.tscclean.f.price:gt=130deep-link: rows hydrate correctly, filtered + sorted, no console errors.Notes
examples/nextjs-app/.../data-table-demo.tsxis updated to the new hook — required by the export change, not optional.🤖 Generated with Claude Code