From d0462a87c8549e6eaedb513d80c03aecaba31d5c Mon Sep 17 00:00:00 2001 From: Richie Date: Thu, 23 Apr 2026 11:23:36 +1000 Subject: [PATCH] Basket persists across in-app navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useBasketUrlSync was treating every searchParams change as a URL→Store authority event. In practice this meant Back from PackagesStep to the providers map landed on `/` (no `?compare=...`) and the hook called setAll([]) — wiping the basket. Changed the semantics so that when an in-app navigation drops the `?compare=` param but the store still has items, we re-attach the store's keys to the new URL rather than clearing the store. Shared links still hydrate the store on initial mount, and the subscribe that writes store→URL on basket changes is untouched. With this, a user can: - Add a package on Provider A's page. - Back to the providers map (CompareBar stays, URL still shows `?compare=parsons:everyday`). - Navigate into Provider B's page (URL carries the Parsons item forward). - Add B's package (URL now `?compare=parsons:everyday,rankins:standard`). - Hit Compare with 2/3 basket. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/demo/shared/state/useBasketUrlSync.ts | 33 +++++++++++++++++++---- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/demo/shared/state/useBasketUrlSync.ts b/src/demo/shared/state/useBasketUrlSync.ts index fd872cb..98c641d 100644 --- a/src/demo/shared/state/useBasketUrlSync.ts +++ b/src/demo/shared/state/useBasketUrlSync.ts @@ -18,8 +18,14 @@ const deserialise = (raw: string | null): string[] => * * Mount once near the router root. URL is the source of truth on initial load * (so a shared link restores the basket); after that, store changes write - * through to the URL and external URL changes (back/forward, manual edits) - * push back into the store. + * through to the URL so the current basket is always shareable. + * + * In-app navigation from a page that carries `?compare=...` to one that + * doesn't (e.g. Back from PackagesStep to the providers map) would drop the + * param — to avoid wiping the store, we re-attach the store's keys to the + * new URL instead of treating the empty URL as a "clear" signal. External + * URL changes that DO carry params still push back into the store (shared + * links, manual edits, browser Back after a store write). */ export function useBasketUrlSync(): void { const [searchParams, setSearchParams] = useSearchParams(); @@ -37,10 +43,27 @@ export function useBasketUrlSync(): void { return; } - if (serialise(urlKeys) !== serialise(storeKeys)) { - useComparisonBasket.getState().setAll(urlKeys); + if (serialise(urlKeys) === serialise(storeKeys)) return; + + // URL empty + store has items → in-app navigation dropped the param. + // Re-attach the store's keys so the basket stays sticky across routes + // (and the current URL remains shareable). + if (urlKeys.length === 0 && storeKeys.length > 0) { + setSearchParams( + (current) => { + const next = new URLSearchParams(current); + next.set(PARAM, serialise(storeKeys)); + return next; + }, + { replace: true }, + ); + return; } - }, [searchParams]); + + // Otherwise URL is authoritative (shared link, manual edit, browser Back + // after a store write) — push it into the store. + useComparisonBasket.getState().setAll(urlKeys); + }, [searchParams, setSearchParams]); useEffect(() => { return useComparisonBasket.subscribe((state, prev) => {