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
+30 -9
View File
@@ -109,15 +109,36 @@ information when forced into a single selector.
derivations `useIsUserIgnored`, `useGroupBadge`), each with defensive
`typeof` guards against a stale renderer bundle.
**Note (2026-05-18):** the first three pilot migrations (`useSessionInfo`,
`useChatWidget.ownUserId`, `AvatarInfoWidgetAvatarView` Ignore-menu)
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 defensive guards (`c35a2d4`) could
not eliminate. Suspected interaction: `useBetween` +
`useSyncExternalStore` + React Compiler. Before retrying any
migration here, exercise the snapshot hooks from a non-`useBetween`
consumer in a low-blast-radius widget first to isolate the cause.
**Hard constraint — snapshot hooks must run outside `useBetween`.**
`use-between` 1.x swaps the React dispatcher with its own proxy
(`ownDispatcher` at
`node_modules/use-between/release/index.esm.js:54-169`) that
reimplements only useState / useReducer / useEffect /
useLayoutEffect / useCallback / useMemo / useRef /
useImperativeHandle. `useSyncExternalStore` is not on the list, so
calling a snapshot hook inside `useBetween(stateFn)` invokes
`undefined(...)` and crashes the first render with
"(intermediate value)() is undefined" (Firefox) /
"dispatcher.useSyncExternalStore is not a function" (Chrome). This
is what blocked the original 2026-05-18 migration of
`useSessionInfo` — the rollback (`e142efd`) was correct as a stop
the bleed, but neither the vite alias (`790ad2b`) nor the
defensive renderer-method guards (`c35a2d4`) could address it
because both were downstream of the dispatcher proxy.
**Fix landed 2026-05-19 (`d28819d`).** Three pilot consumers shipped:
`useSessionInfo` (snapshot read in the outer wrapper, after
`useBetween`); `useChatWidget.ownUserId` (direct hook call —
`useChatWidget` is not wrapped in `useBetween`);
`AvatarInfoWidgetAvatarView` Ignore/Unignore (direct hook call in a
component body via `useIsUserIgnored`). Pattern documented in
`CLAUDE.md` under "Patterns to use →
`useSessionSnapshots`". Regression guard:
`src/hooks/session/useSessionSnapshots.test.tsx` (negative case via
`ErrorBoundary` + positive case). CI gate:
`yarn lint:hooks` (`eslint.hooks.config.mjs`
`react-hooks/rules-of-hooks: error`) wired into
`.github/workflows/ci.yml`.
For state owned outside the listener (the `useState` + `setState(prev =>
applyX(prev, event))` pattern), keep using `useNitroEvent` /