Skip to content

feat: add URL state persistence of CollectionControl#293

Open
IzumiSy wants to merge 6 commits into
mainfrom
improve-use-url-collection-state
Open

feat: add URL state persistence of CollectionControl#293
IzumiSy wants to merge 6 commits into
mainfrom
improve-use-url-collection-state

Conversation

@IzumiSy

@IzumiSy IzumiSy commented May 27, 2026

Copy link
Copy Markdown
Contributor

Overview

A feature to persist CollectionControl state (filters, sort, page size) to URL query parameters.

This enables:

  • Bookmarkable page state — Users can share or save URLs with applied filters and sort
  • Browser back/forward support — State is restored through URL history
  • Entity-agnostic design — Uses short key names and automatic encoding derived from the Filter shape, making it usable with any resource

Cursor/pagination direction state is intentionally not persisted — a page refresh resets to page 1.

@IzumiSy IzumiSy changed the title improve: simplify and harden useUrlCollectionState feat: add useUrlCollectionState hook May 27, 2026
@interacsean

Copy link
Copy Markdown
Contributor

Heads-up from the downstream side 👋 — we've been running this hook in the Denim Tears IMS app (apps/ims/frontend/src/hooks/use-url-collection-state.ts), and this PR looks like it was derived from a mid-May snapshot of it (same doc comment, p/s/f. keys). We landed a few fixes on our copy on June 2, after this PR opened (May 27), that are worth folding in here — one is a real bug.

1. between (object-valued) filters don't round-trip 🐛

encodeFilterValue writes objects as JSON, but decodeFilterValue only parses arrays back — objects fall through to the raw string:

const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) return parsed;   // objects dropped → returned as a string
return raw;

The Filter type here defines between{ min, max } (OperatorValueType), so any between filter (date ranges, numeric ranges) gets written to the URL as {"min":…,"max":…} and comes back as a string on reload — the filter silently breaks. One-line fix:

const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) return parsed;
if (parsed && typeof parsed === "object") return parsed;  // round-trip between/{min,max}
return raw;

(Primitives like "5"/"true" parse but aren't objects, so they still return as strings — string-vs-numeric filter semantics preserved.)

2. Equality check is order- / &=-sensitive (robustness, lower priority)

next.toString() === prev.toString() is sensitive to key-insertion order and to &/= inside values. We switched to a sorted, JSON-encoded snapshot of the entries so reordered or edge-char params compare equal. Your functional-updater approach (omitting params from the deps) already sidesteps the churn loop this originally guarded against, so it's more of a latent sharp edge than a live bug — flagging in case it's cheap to fold in.

3. Multi-value encoding — heads-up, not a request

We independently fixed the same comma-splitting bug ("Apparel, LLC"["Apparel"," LLC"]), but chose repeated params (f.status:in=a&f.status:in=b) where you chose a JSON array (f.status:in=["a","b"]). Both are valid. Yours actually keeps single-element arrays as arrays (ours round-trips them to scalars and re-wraps downstream), so no change requested — just flagging that whichever format ships becomes the URL contract consumers bookmark against.

Nice to see the test coverage — our copy has none, so we'll likely adopt this hook and retire ours once it lands. Only #1 is a must-fix from our experience.

@pkg-pr-new

pkg-pr-new Bot commented Jun 15, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/tailor-platform/app-shell/@tailor-platform/app-shell@293
npm i https://pkg.pr.new/tailor-platform/app-shell/@tailor-platform/app-shell-sdk-plugin@293
npm i https://pkg.pr.new/tailor-platform/app-shell/@tailor-platform/app-shell-vite-plugin@293

commit: 82856e6

IzumiSy and others added 2 commits June 19, 2026 15:19
- 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>
@IzumiSy IzumiSy force-pushed the improve-use-url-collection-state branch from 82856e6 to c25039d Compare June 19, 2026 06:19
@IzumiSy IzumiSy changed the title feat: add useUrlCollectionState hook feat: add URL state persistence of CollectionControl Jun 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants