catalog: three-way singleton-filter split + first 3 consumer migrations

Completes the useCatalog decomposition. After the previous commit
extracted the pure helpers, this one splits the singleton-via-useBetween
store into three slice-specific entry points and migrates a handful of
consumers as proof.

`src/hooks/catalog/useCatalog.ts`

- Internal `useCatalogState` → renamed to `useCatalogStore` and is no
  longer exported. The full return shape is unchanged so callers that
  still go through the shim see the exact same object.
- Three new exports built on top of the same `useBetween` instance:
    - `useCatalogData()` — server-driven read-only slice (rootNode,
      offersToNodes, currentPage, currentOffer, frontPageItems,
      searchResult, roomPreviewer, isBusy, catalog localization
      version, Builders Club counters + timers).
    - `useCatalogUiState()` — UI ephemeral state + writers
      (isVisible, pageId, previousPageId, currentType, activeNodes,
      navigationHidden, purchaseOptions, catalogPlaceMultipleObjects,
      plus every `set*` writer including the ones that mutate the
      data slice on user-driven selection).
    - `useCatalogActions()` — imperative operations only
      (openCatalogByType, toggleCatalogByType, activateNode,
      openPageBy{Id,Name,OfferId}, requestOfferToMover,
      selectCatalogOffer, getNodeBy{Id,Name},
      getBuilderFurniPlaceableStatus).
- `useCatalog` is kept as a deprecated shim that returns the full
  historical surface, so the 48 existing consumers compile and run
  unchanged.

Pilot consumer migrations (3 of 48):

- `CatalogBuildersClubStatusView` — Data (furni counters, seconds
  timers) + UiState (currentType).
- `CatalogBreadcrumbView` — UiState (activeNodes) + Actions
  (activateNode).
- `CatalogNavigationItemView` — UiState (currentType) + Actions
  (activateNode).

Tests: `tests/useCatalog.filters.test.tsx` (5 cases).

`useBetween` is mocked via `vi.hoisted` so the four hooks share one
deterministic fake store — rendering the real `useCatalogStore`
would mount ~30 useState calls + open a fresh RoomPreviewer +
subscribe to a dozen renderer events, which is more than these
contract tests need.

- `useCatalogData` exposes exactly its read-only keys.
- `useCatalogUiState` exposes exactly its UI keys + setters.
- `useCatalogActions` exposes exactly its imperative ops (and
  explicitly NOT data fields — proves no leak across slices).
- Singleton identity: callbacks read through the shim are `===` to
  the ones read through the slices.
- Shim surface: the historical key set is still present so
  un-migrated consumers don't silently break.

Suite: 163/163 (was 158/158). `yarn typecheck` green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
simoleo89
2026-05-13 21:50:56 +02:00
parent fd3ef7875d
commit 59d6c4cab3
7 changed files with 372 additions and 15 deletions
+40 -4
View File
@@ -480,9 +480,39 @@ Status after this round of work:
| CatalogPagesList / CatalogPage | **deferred** — core state slice (rootNode / offersToNodes / currentPage), needs its own split-out store |
| BuildersClubFurniCount / SubscriptionStatus | **deferred** — read by the internal `getBuilderFurniPlaceableStatus` logic, moves with the data/actions split |
Pure-helper extraction landed before the singleton split:
`src/hooks/catalog/useCatalog.helpers.ts` hosts the dependency-free
pieces previously inlined in the hook —
**Helper extraction + filter split both landed.** The 1100-line hook
now has its dependency-free logic in
`src/hooks/catalog/useCatalog.helpers.ts` and exposes three public
filters built on top of the same `useBetween` singleton:
- `useCatalogData()` — server-driven read-only slice (`rootNode`,
`offersToNodes`, `currentPage`, `currentOffer`, `frontPageItems`,
`searchResult`, `roomPreviewer`, `isBusy`,
`catalogLocalizationVersion`, Builders Club counters + timers).
- `useCatalogUiState()` — UI ephemeral state + writers
(`isVisible`, `pageId`, `previousPageId`, `currentType`,
`activeNodes`, `navigationHidden`, `purchaseOptions`,
`catalogPlaceMultipleObjects`, plus all the `set*` writers,
including the ones that mutate the data slice on page / offer /
search-result selection).
- `useCatalogActions()` — imperative operations
(`openCatalogByType`, `toggleCatalogByType`, `activateNode`,
`openPageBy{Id,Name,OfferId}`, `requestOfferToMover`,
`selectCatalogOffer`, `getNodeBy{Id,Name}`,
`getBuilderFurniPlaceableStatus`).
The internal store is named `useCatalogStore` and is **not exported**;
the four public entry points (`useCatalogData` / `useCatalogUiState`
/ `useCatalogActions` / `useCatalog`) all funnel into the same
`useBetween` instance, so listeners + state register once. The
deprecated `useCatalog` shim continues to expose the full historical
return shape so the 48 existing consumers compile unchanged; they
should be incrementally migrated to the specific filters as PRs
touch them. Three pilot migrations already landed in
`CatalogBuildersClubStatusView`, `CatalogBreadcrumbView`, and
`CatalogNavigationItemView`.
Pure helpers in `useCatalog.helpers.ts`:
- `normalizeCatalogType(type?)` — coerce the optional catalog type
back to `NORMAL` / `BUILDER`.
@@ -512,7 +542,7 @@ empty-map / partial-bucket branches of the offer lookup).
- Vitest 3 + jsdom + `@testing-library/react` + `@testing-library/jest-dom`
configured. Separate `vitest.config.mts` so the runner doesn't drag in
the renderer SDK aliases from `vite.config.mjs`.
- **158 cases passing** across 11 test files. Pure-module suites:
- **163 cases passing** across 12 test files. Pure-module suites:
- `WiredCreatorTools.helpers.test.ts` (18) — formatters + snapshot
factory.
- `navigatorRoomCreatorStore.test.ts` (4) — Zustand store invariants
@@ -539,6 +569,12 @@ empty-map / partial-bucket branches of the offer lookup).
the partial-visible fallback), `buildCatalogNodeTree` (tree
depth + offerId index), and the full decision tree of
`resolveBuilderFurniPlaceableStatus`.
- `useCatalog.filters.test.tsx` (5) — contract tests for the
three-way singleton-filter split. Stubs `use-between` so the
filters share one fake store, asserts each filter exposes
exactly the keys it owns (no leak across slices), and pins
down `===` identity of callbacks between the shim and each
slice so the migration of the 48 consumers stays safe.
Component-/hook-level suites (on the new renderer-SDK mock):
- `WidgetErrorBoundary.test.tsx` (4) — happy path + caught render