feat: add URL state persistence of CollectionControl#293
Conversation
|
Heads-up from the downstream side 👋 — we've been running this hook in the Denim Tears IMS app ( 1.
|
commit: |
82856e6 to
c25039d
Compare
|
After comparing this with the old The old design hydrated internal collection state after mount and synchronized it back to the URL through effects. While functional, that approach leans on post-render state synchronization in a way that is less aligned with the React style we should aim for. The current API is closer to the React way:
This gives clearer initialization semantics and avoids effect-based external-to-internal state synchronization after mount. I also think this matters from a maintenance and tooling perspective. If we keep introducing patterns like the old one, they are more likely to be repeated by AI-assisted development as well. I’d rather have the codebase teach and reinforce the React-idiomatic pattern wherever we can. |
- 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
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>
80da0f9 to
874480d
Compare
…+ 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>
Overview
A feature to persist
CollectionControlstate (filters, sort, page size) to URL query parameters.This enables:
Cursor/pagination direction state is intentionally not persisted — a page refresh resets to page 1.