docs(claude,architecture): refresh snapshot adoption status after 2026-05-19 fix

The earlier "BLOCKED" / "rolled back" framing in CLAUDE.md +
ARCHITECTURE.md is stale: the three pilot snapshot-consumer migrations
shipped in d28819d on 2026-05-19 once the root cause was pinpointed
(`use-between` 1.x ships a dispatcher proxy that doesn't implement
`useSyncExternalStore`, so any snapshot hook called inside
useBetween(stateFn) crashes the first render).

Updated:

- CLAUDE.md → "Patterns to use → useSessionSnapshots": rewrote the
  adoption-status paragraph to record the three live consumers, the
  hard structural constraint (snapshot reads MUST be outside
  useBetween scope, with the precise dispatcher line numbers + the
  exact error fingerprint), and the fix template applied to
  useSessionInfo (outer wrapper reads the snapshot, inner state
  function keeps only use-between-safe hooks).

- CLAUDE.md → "What's wired up and what isn't" tables:
  - Adopted row for "Renderer snapshot consumer hooks" lists the
    three live consumers instead of the old "No in-tree consumers"
    note.
  - "Not yet" row renamed from "Blocked" to "Unblocked — migrate more
    consumers", with concrete next candidates
    (GetSessionDataManager().userId / userName / clubLevel /
    securityLevel, GetRoomSessionManager().getActiveSession(),
    GetSoundManager().<volume>) and a reminder of the constraint
    + the CI gate that enforces it.
  - useChatWidget.ownUserId row notes the reactive migration via
    useUserDataSnapshot landed (direct hook call — useChatWidget
    isn't wrapped in useBetween, so the constraint doesn't apply).

- ARCHITECTURE.md → "useExternalSnapshot" subsection: replaced the
  2026-05-18 rollback note with the structural constraint + the
  2026-05-19 fix landing, including pointers to the regression test
  and the new CI gate (eslint.hooks.config.mjs + yarn lint:hooks).

No code change in this commit — yarn typecheck clean, yarn
lint:hooks clean.
This commit is contained in:
simoleo89
2026-05-19 18:01:04 +02:00
parent a029ee63cb
commit 3459400ed7
2 changed files with 81 additions and 22 deletions
+51 -13
View File
@@ -150,16 +150,54 @@ a stale renderer bundle and degrades to a frozen default snapshot if
the renderer doesn't expose the matching getter (kept module-level so
React's bailout still works on the degraded path).
**Adoption status: zero in-tree consumers.** The first three pilot
migrations (`useSessionInfo`, `useChatWidget.ownUserId`,
`AvatarInfoWidgetAvatarView` Ignore/Unignore) were rolled back in
`e142efd` after a persistent runtime error
`(intermediate value)() is undefined` at `ToolbarView.tsx:46` that the
vite-alias fix (`790ad2b`) and the defensive guards (`c35a2d4`) could
not eliminate. The hooks remain available for any future opt-in
consumer, but **do not migrate `useBetween`-shared consumers to them
without isolated testing first** — that combination is the suspected
cause of the bug.
**Adoption status: three pilot consumers shipped (commit `d28819d`,
2026-05-19).** `useSessionInfo` reads userFigure / respectsLeft /
respectsPetLeft from `useUserDataSnapshot`; `useChatWidget.ownUserId`
reads from the snapshot directly; `AvatarInfoWidgetAvatarView` flips
its Ignore/Unignore menu via `useIsUserIgnored`.
The original rollback (`e142efd`) was caused by a hard structural
constraint, NOT a stale renderer or React Compiler quirk: **snapshot
hooks (`useSyncExternalStore`-based) must NOT be called inside a
`useBetween(stateFn)` scope.** `use-between` 1.x swaps
`ReactCurrentDispatcher.current` with its own proxy
(`ownDispatcher` at
`node_modules/use-between/release/index.esm.js:54-169`) that
re-implements only useState / useReducer / useEffect /
useLayoutEffect / useCallback / useMemo / useRef /
useImperativeHandle. `useSyncExternalStore` isn't on the list, so
React resolves `dispatcher.useSyncExternalStore` to `undefined` and
crashes on first paint — that's the original "(intermediate value)()
is undefined" at `ToolbarView.tsx:46`. Chrome reports the same as
`dispatcher.useSyncExternalStore is not a function`.
**Fix pattern, applied to `useSessionInfo`:** call the snapshot hook
in the OUTER exported wrapper, after `useBetween`, so it runs in the
real React dispatcher's scope. The inner state function (the one
`useBetween` actually proxies) keeps only useState /
useMessageEvent / plain actions.
```ts
const useSessionInfoState = () => {
// ONLY use-between-safe hooks here.
const [chatStyleId, setChatStyleId] = useState(0);
// … useMessageEvent, actions …
return { chatStyleId, /* actions */ };
};
export const useSessionInfo = () => {
const shared = useBetween(useSessionInfoState);
const userData = useUserDataSnapshot(); // outside useBetween → ok
return { ...shared, userFigure: userData.figure, /* etc */ };
};
```
Regression guard: `src/hooks/session/useSessionSnapshots.test.tsx`
asserts the negative case (snapshot inside useBetween crashes via
ErrorBoundary) and the positive case (outside works). A CI gate
(`yarn lint:hooks` →
`react-hooks/rules-of-hooks: error`) blocks any future commit that
reintroduces hook-order issues.
### `useNitroEventState` / `useMessageEventState`
@@ -294,7 +332,7 @@ into `configurePreviewServer` so `yarn preview` keeps working.
| Adopted | Pilot sites |
|---|---|
| Renderer snapshot consumer hooks (`useSessionSnapshots`) | **No in-tree consumers** — three pilot migrations rolled back in `e142efd` due to a runtime `(intermediate value)() is undefined` at `ToolbarView.tsx:46` that survived both the vite-alias fix and defensive guards. The 8 hooks (userData / activeRoomSession / ignoredUsers / groupBadges / soundVolumes / roomUserList / isUserIgnored / groupBadge) remain available as opt-in API with frozen-default fallbacks. |
| Renderer snapshot consumer hooks (`useSessionSnapshots`) | `useSessionInfo` (userFigure / userRespectRemaining / petRespectRemaining via `useUserDataSnapshot` in the outer wrapper, outside useBetween), `useChatWidget.ownUserId` (via `useUserDataSnapshot`), `AvatarInfoWidgetAvatarView` Ignore/Unignore (via `useIsUserIgnored`). The 8 hooks (userData / activeRoomSession / ignoredUsers / groupBadges / soundVolumes / roomUserList / isUserIgnored / groupBadge) keep their typeof-guard defensive fallbacks for stale-renderer paths. |
| `useNitroEventState` + companions (Reducer, ExternalSnapshot) | `OfferView`, `useAvatarInfoWidget` (figure/badges/group reducer), `useInventoryFurni` (pure reducers + fragments useRef) |
| `useNitroQuery` + `useNitroEventInvalidator` | `OfferView`, `CatalogLayoutRoomAdsView`, `ModToolsChatlogView`, `CfhChatlogView`, `useGiftConfiguration`, `useUserGroups`, `useClubOffers(windowId)`, `useSellablePetPalette(breed)`, `useMarketplaceConfiguration`, `useClubGifts` (with invalidator) |
| Zustand | `NavigatorRoomCreatorView` (`useRoomCreatorStore`), `WiredCreatorToolsView` (`useWiredCreatorToolsUiStore` — every panel-lifecycle-relevant flag, snapshot, selection, highlight, inline editor, picker chain hoisted; what's left in the component as `useState` is genuinely transient: keepSelected, globalClock, roomEnteredAt, selectedMonitorErrorType, selectedMonitorLogDetails) |
@@ -307,9 +345,9 @@ into `configurePreviewServer` so `yarn preview` keeps working.
| Not yet | Notes |
|---|---|
| Split `useChatWidget` / `useAvatarInfoWidget` (data/actions) | Both state-driven via events with no clean imperative actions to extract — split still skip-motivated, but `useAvatarInfoWidget` got a typed `__nitroAvatarClickControl` accessor + module-scope DEBOUNCE const in 2026-05-18 (commit `05ff7df`). The `useChatWidget` reactive-`ownUserId` migration in the same commit was rolled back in `e142efd`; the hook is back on `GetSessionDataManager()?.userId` (static at mount). |
| Split `useChatWidget` / `useAvatarInfoWidget` (data/actions) | Both state-driven via events with no clean imperative actions to extract — split still skip-motivated, but `useAvatarInfoWidget` got a typed `__nitroAvatarClickControl` accessor + module-scope DEBOUNCE const in 2026-05-18 (commit `05ff7df`). `useChatWidget.ownUserId` reactive migration re-applied 2026-05-19 in `d28819d` via `useUserDataSnapshot` (direct hook call — useChatWidget isn't wrapped in useBetween so the snapshot-outside-useBetween constraint doesn't apply). |
| Split `usePetPackageWidget` / `useWordQuizWidget` / `useChatCommandSelector` (data/actions) | Data/actions split remains a bad fit, but all three got real modernization in 2026-05-18 instead: usePetPackageWidget → useReducer + extracted `getPetPackageNameError` pure helper + 4 tests; useWordQuizWidget → fixed stale-closure bug in `setUserAnswers` updater + `useRef` for the timeout handle; useChatCommandSelector → module-level `let` cache replaced with a Zustand store. |
| Migrate any consumer to renderer snapshot hooks | **Blocked.** Three pilot migrations were rolled back in `e142efd` after the `useSessionInfo` migration triggered a persistent runtime error at `ToolbarView.tsx:46`. The defensive guards in `useSessionSnapshots.ts` and the umbrella vite alias (`790ad2b`) are still in place. Before retrying, isolate the cause: the suspected interaction is `useBetween` + `useSyncExternalStore` + React Compiler. Try a NON-`useBetween` consumer first (e.g. a fresh per-component hook usage in a low-blast-radius widget). |
| Migrate more consumers to renderer snapshot hooks | **Unblocked.** Three pilot consumers shipped 2026-05-19 (`d28819d`), pattern documented above. Next candidates: any code reading from `GetSessionDataManager().userId / userName / clubLevel / securityLevel`, `GetRoomSessionManager().getActiveSession()`, or `GetSoundManager().<volume>` synchronously — those don't re-render today when the value changes. Rule: snapshot read MUST be outside any `useBetween` scope (CI gate `yarn lint:hooks` catches violations; regression test at `src/hooks/session/useSessionSnapshots.test.tsx`). |
| Widen the component / hook test coverage | Mock layer is in place (`src/nitro-renderer.mock.ts`) and 3+ hook/component pilots pass. Good follow-up targets: `LoginView` Form Actions happy/error paths, `OfferView` with `useNitroQuery`. (Acceptable only as a side-effect of a real change — coverage growth on its own is deprioritized per session feedback.) |
## Known open logic bugs