mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 23:16:21 +00:00
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:
@@ -12,7 +12,15 @@ import { useCatalogSkipPurchaseConfirmation } from './useCatalogSkipPurchaseConf
|
||||
const DUMMY_PAGE_ID_FOR_OFFER_SEARCH = -12345678;
|
||||
const DRAG_AND_DROP_ENABLED = true;
|
||||
|
||||
const useCatalogState = () =>
|
||||
// Internal singleton store — held together by `useBetween` so every
|
||||
// public filter below sees the same listeners + state. Do NOT export
|
||||
// this directly; consumers must go through the filters or the
|
||||
// deprecated `useCatalog` shim. The previous 1100-line monolith
|
||||
// exposed everything via `useCatalog`; the three filters below
|
||||
// (`useCatalogData` / `useCatalogUiState` / `useCatalogActions`)
|
||||
// shrink the surface each consumer subscribes to, which lets the
|
||||
// React Compiler memoize and avoids unrelated re-renders.
|
||||
const useCatalogStore = () =>
|
||||
{
|
||||
const [ isVisible, setIsVisible ] = useState(false);
|
||||
const [ isBusy, setIsBusy ] = useState(false);
|
||||
@@ -958,4 +966,105 @@ const useCatalogState = () =>
|
||||
return { isVisible, setIsVisible, isBusy, pageId, previousPageId, currentType, rootNode, offersToNodes, currentPage, setCurrentPage, currentOffer, setCurrentOffer, activeNodes, searchResult, setSearchResult, frontPageItems, roomPreviewer, navigationHidden, setNavigationHidden, purchaseOptions, setPurchaseOptions, catalogLocalizationVersion, getNodeById, getNodeByName, activateNode, openPageById, openPageByName, openPageByOfferId, requestOfferToMover, openCatalogByType, toggleCatalogByType, furniCount, furniLimit, maxFurniLimit, secondsLeft, secondsLeftWithGrace, updateTime, catalogPlaceMultipleObjects, setCatalogPlaceMultipleObjects, getBuilderFurniPlaceableStatus, selectCatalogOffer };
|
||||
};
|
||||
|
||||
export const useCatalog = () => useBetween(useCatalogState);
|
||||
/**
|
||||
* Read-only slice of server-driven catalog state. Anything a consumer
|
||||
* needs to *display* (page tree, current page, offers, Builders Club
|
||||
* counters) lives here.
|
||||
*
|
||||
* `roomPreviewer` and the busy flag are kept here too because they
|
||||
* are observed (not mutated) by every consumer that renders a preview.
|
||||
*/
|
||||
export const useCatalogData = () =>
|
||||
{
|
||||
const {
|
||||
isBusy,
|
||||
rootNode, offersToNodes,
|
||||
currentPage, currentOffer,
|
||||
frontPageItems, searchResult,
|
||||
roomPreviewer,
|
||||
catalogLocalizationVersion,
|
||||
furniCount, furniLimit, maxFurniLimit,
|
||||
secondsLeft, secondsLeftWithGrace, updateTime
|
||||
} = useBetween(useCatalogStore);
|
||||
|
||||
return {
|
||||
isBusy,
|
||||
rootNode, offersToNodes,
|
||||
currentPage, currentOffer,
|
||||
frontPageItems, searchResult,
|
||||
roomPreviewer,
|
||||
catalogLocalizationVersion,
|
||||
furniCount, furniLimit, maxFurniLimit,
|
||||
secondsLeft, secondsLeftWithGrace, updateTime
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* UI-side state owned by the catalog overlay itself: visibility, the
|
||||
* currently-rendered page id and breadcrumb, search query result,
|
||||
* purchase options, multi-place toggle. Includes the setters that
|
||||
* mutate the data slice when the user picks a page / offer / search
|
||||
* result — those don't trigger server traffic so they belong to the
|
||||
* UI layer.
|
||||
*/
|
||||
export const useCatalogUiState = () =>
|
||||
{
|
||||
const {
|
||||
isVisible, setIsVisible,
|
||||
pageId, previousPageId,
|
||||
currentType,
|
||||
activeNodes,
|
||||
navigationHidden, setNavigationHidden,
|
||||
purchaseOptions, setPurchaseOptions,
|
||||
catalogPlaceMultipleObjects, setCatalogPlaceMultipleObjects,
|
||||
setCurrentPage, setCurrentOffer, setSearchResult
|
||||
} = useBetween(useCatalogStore);
|
||||
|
||||
return {
|
||||
isVisible, setIsVisible,
|
||||
pageId, previousPageId,
|
||||
currentType,
|
||||
activeNodes,
|
||||
navigationHidden, setNavigationHidden,
|
||||
purchaseOptions, setPurchaseOptions,
|
||||
catalogPlaceMultipleObjects, setCatalogPlaceMultipleObjects,
|
||||
setCurrentPage, setCurrentOffer, setSearchResult
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Imperative actions: open / toggle the catalog, navigate the page
|
||||
* tree, request a furni to the mover, look up nodes by id/name, run
|
||||
* the Builders Club placement check. These all either send a
|
||||
* composer to the server, dispatch a UI event, or run synchronous
|
||||
* tree queries — none of them are React state by themselves.
|
||||
*/
|
||||
export const useCatalogActions = () =>
|
||||
{
|
||||
const {
|
||||
openCatalogByType, toggleCatalogByType,
|
||||
activateNode,
|
||||
openPageById, openPageByName, openPageByOfferId,
|
||||
requestOfferToMover, selectCatalogOffer,
|
||||
getNodeById, getNodeByName,
|
||||
getBuilderFurniPlaceableStatus
|
||||
} = useBetween(useCatalogStore);
|
||||
|
||||
return {
|
||||
openCatalogByType, toggleCatalogByType,
|
||||
activateNode,
|
||||
openPageById, openPageByName, openPageByOfferId,
|
||||
requestOfferToMover, selectCatalogOffer,
|
||||
getNodeById, getNodeByName,
|
||||
getBuilderFurniPlaceableStatus
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Deprecated. Kept so the 48 existing consumers compile unchanged —
|
||||
* incrementally migrate them to `useCatalogData` / `useCatalogUiState`
|
||||
* / `useCatalogActions` and remove this shim once the call sites are
|
||||
* gone. Mirrors the same `useBetween` singleton, so behavior is
|
||||
* identical.
|
||||
*/
|
||||
export const useCatalog = () => useBetween(useCatalogStore);
|
||||
|
||||
Reference in New Issue
Block a user