mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
d28819db89
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.
62 lines
2.4 KiB
TypeScript
62 lines
2.4 KiB
TypeScript
import { GetSessionDataManager, RoomUnitChatStyleComposer, UserInfoDataParser, UserInfoEvent, UserSettingsEvent } from '@nitrots/nitro-renderer';
|
|
import { useState } from 'react';
|
|
import { useBetween } from 'use-between';
|
|
import { SendMessageComposer } from '../../api';
|
|
import { useMessageEvent } from '../events';
|
|
import { useUserDataSnapshot } from './useSessionSnapshots';
|
|
|
|
// State function — ONLY use-between-safe hooks here (useState,
|
|
// useMessageEvent, plain actions). Do NOT call snapshot hooks here:
|
|
// use-between's dispatcher does not implement useSyncExternalStore, so
|
|
// any `useUserDataSnapshot()` / `useExternalSnapshot()` call inside
|
|
// this body crashes the React tree on first paint with
|
|
// "(intermediate value)() is undefined". See useSessionSnapshots.test.tsx
|
|
// for the regression guard.
|
|
const useSessionInfoState = () =>
|
|
{
|
|
const [ userInfo, setUserInfo ] = useState<UserInfoDataParser>(null);
|
|
const [ chatStyleId, setChatStyleId ] = useState<number>(0);
|
|
|
|
const updateChatStyleId = (styleId: number) =>
|
|
{
|
|
setChatStyleId(styleId);
|
|
|
|
SendMessageComposer(new RoomUnitChatStyleComposer(styleId));
|
|
};
|
|
|
|
const respectUser = (userId: number) => GetSessionDataManager().giveRespect(userId);
|
|
const respectPet = (petId: number) => GetSessionDataManager().givePetRespect(petId);
|
|
|
|
useMessageEvent<UserInfoEvent>(UserInfoEvent, event =>
|
|
{
|
|
setUserInfo(event.getParser().userInfo);
|
|
});
|
|
|
|
useMessageEvent<UserSettingsEvent>(UserSettingsEvent, event =>
|
|
{
|
|
setChatStyleId(event.getParser().chatType);
|
|
});
|
|
|
|
return { userInfo, chatStyleId, respectUser, respectPet, updateChatStyleId };
|
|
};
|
|
|
|
// Public surface — snapshot reads happen in the OUTER wrapper, in the
|
|
// real React dispatcher's scope, so useSyncExternalStore installs
|
|
// correctly. useBetween only proxies the non-snapshot slice, where its
|
|
// dispatcher works fine. SessionDataManager already invalidates the
|
|
// snapshot on UserInfoEvent / FigureUpdateEvent / giveRespect /
|
|
// givePetRespect, so userFigure / respectsLeft / respectsPetLeft stay
|
|
// in sync without local useState mirrors.
|
|
export const useSessionInfo = () =>
|
|
{
|
|
const shared = useBetween(useSessionInfoState);
|
|
const userData = useUserDataSnapshot();
|
|
|
|
return {
|
|
...shared,
|
|
userFigure: userData.figure,
|
|
userRespectRemaining: userData.respectsLeft,
|
|
petRespectRemaining: userData.respectsPetLeft
|
|
};
|
|
};
|