Root cause of last session's "(intermediate value)() is undefined" at
ToolbarView.tsx:46:
use-between 1.x ships its own React-dispatcher proxy (ownDispatcher
in node_modules/use-between/release/index.esm.js:54-169) that
re-implements only useState, useReducer, useEffect, useLayoutEffect,
useCallback, useMemo, useRef and useImperativeHandle. It does NOT
implement useSyncExternalStore. When the inner state function of
useBetween(stateFn) calls useSyncExternalStore (directly or via
useExternalSnapshot / useUserDataSnapshot), React resolves the
dispatcher to use-between's proxy, finds .useSyncExternalStore
missing, and calls undefined() — that's the exact production crash
in Firefox. Chrome reports the same as
"dispatcher.useSyncExternalStore is not a function".
Neither the vite alias (790ad2b) nor the defensive renderer-method
guards (c35a2d4) could fix it — both addressed downstream symptoms
(stale dist / missing manager methods) but the dispatcher is upstream
of both. That's why every retry kept reproducing the same error.
Fix is structural: snapshot hooks (useUserDataSnapshot,
useIsUserIgnored, etc.) MUST run outside any useBetween scope. Three
re-applied migrations:
- useSessionInfo: snapshot read moved into the outer wrapper. The
inner useSessionInfoState (useBetween-shared) now contains only
use-between-safe hooks: useState, useMessageEvent, plain actions.
userFigure / userRespectRemaining / petRespectRemaining come from
useUserDataSnapshot() OUTSIDE useBetween, so useSyncExternalStore
installs against the real React dispatcher.
- useChatWidget.ownUserId: direct snapshot read. useChatWidget is
exported as `useChatWidget = useChatWidgetState` (NOT wrapped in
useBetween), so this hook never sat inside the broken scope — the
precautionary rollback was unnecessary in retrospect. Gains
session-change reactivity (e.g. reconnect under a different user id).
- AvatarInfoWidgetAvatarView Ignore/Unignore: useIsUserIgnored(name)
read directly in the component body. Same reasoning as
useChatWidget — never inside useBetween. The menu auto-flips
Ignore <-> Unignore while the popup is open.
Added regression guard at src/hooks/session/useSessionSnapshots.test.tsx
with two cases: (1) useSyncExternalStore inside useBetween throws,
(2) useSyncExternalStore outside useBetween in the same render works.
Pins the constraint so future migrations cannot reintroduce the bad
shape silently.
Verification: yarn typecheck clean, yarn test 209/209 (207 baseline
+ 2 new regression cases), no consumer surface changes — every
destructured field (userFigure, userRespectRemaining, respectUser,
petRespectRemaining, respectPet, chatStyleId, updateChatStyleId) is
still returned with the same name and shape.
The migrations of useSessionInfo, useChatWidget.ownUserId and the
AvatarInfo Ignore/Unignore menu to the new useSessionSnapshots hooks
were correct in code but produce a persistent runtime error in the
user's deployment:
TypeError: (intermediate value)() is undefined
ToolbarView ToolbarView.tsx:46
The error fires from React's render loop on the first paint —
ToolbarView is the first mounted consumer of useSessionInfo, which is
why it carries the boundary message. Two attempted fixes did not
resolve it on the user's side:
- 790ad2b: vite alias forcing @nitrots/nitro-renderer to source index.ts
- c35a2d4: defensive typeof guards on every Manager method call inside
useSessionSnapshots (so a missing method degrades to a frozen default
rather than calling undefined)
Both are correct defenses but the error persists, meaning the failure
mode is upstream of those guards. Rather than burn more cycles
remote-debugging, roll back the three consumer migrations:
- useSessionInfo: restored to the pre-71a0eee shape — 5 useState
fields driven by useMessageEvent<UserInfoEvent, FigureUpdateEvent,
UserSettingsEvent>. The five consumers (ToolbarView, HcCenterView,
ChatInputView, AvatarInfoPetTrainingPanelView, InfoStandWidgetPetView,
AvatarInfo{Avatar,Pet,OwnPet}View) get the same destructured shape
they had before this session.
- useChatWidget.ownUserId: restored to `GetSessionDataManager()?.userId`
(synchronous, captured at mount). Loses the session-change reactivity
but matches the previous, working behaviour.
- AvatarInfoWidgetAvatarView Ignore/Unignore: restored to
`avatarInfo.isIgnored` (captured by AvatarInfoUtilities at click
time, not reactive). Loses the live-toggle if the user is
ignored/unignored while the popup is open — known small regression,
worth it for stability.
Kept intact:
- The useSessionSnapshots.ts hook file itself, with defensive guards,
so the API stays available for any future opt-in consumer.
- 790ad2b vite alias for the umbrella, still useful as defence in
depth for future migrations.
- All the other non-snapshot modernizations from this session
(usePetPackageWidget reducer, useWordQuizWidget bug fix,
useChatCommandSelector Zustand store, useAvatarInfoWidget typed
globalThis accessor).
Verification: yarn typecheck clean, yarn test 207/207, yarn build green.
The toolbar should boot without the error now — the call chain on the
first paint no longer touches the new useExternalSnapshot / snapshot
getter path.
Replace the local useState mirror of userFigure / userRespectRemaining /
petRespectRemaining (driven by useMessageEvent<UserInfoEvent> +
useMessageEvent<FigureUpdateEvent> + manual setUser after giveRespect)
with a single useUserDataSnapshot() read.
Why this works: SessionDataManager already invalidates its snapshot
on every state change that mattered to the old hook — UserInfoEvent
handler (line 142), FigureUpdateEvent listener (line 117),
giveRespect / givePetRespect (lines 540/551). The snapshot's
respectsLeft / respectsPetLeft map directly to the parser fields
respectsRemaining / respectsPetRemaining the old code mirrored.
Net result: 3 useState declarations + 2 useMessageEvent subscriptions
removed; respectUser / respectPet become trivial pass-throughs (no
post-call setState because the manager's invalidate dispatches the
event for us). UserSettingsEvent stays on useMessageEvent —
chatStyleId is not in the snapshot.
Also drops the deprecated `userInfo: UserInfoDataParser` field from
the return shape — no in-tree consumer reads it (verified via grep
across src/), it was carried as legacy clutter.
Consumers unchanged: ToolbarView, HcCenterView, ChatInputView,
AvatarInfoPetTrainingPanelView, InfoStandWidgetPetView, AvatarInfoWidget
{Avatar,Pet,OwnPet}View. All destructure individual fields, not the
deprecated userInfo.
Verification: yarn typecheck clean, yarn test 203/203.