Enable Zustand (proposal #5) + convert isCreatingRoom singleton

Phase 2 of the refactor plan in docs/ARCHITECTURE.md.

Install
- yarn add zustand (^5, matches React 19 peer requirement).

Wiring
- src/state/createNitroStore.ts: replaces the previous prototype
  (which threw on call) with a re-export of zustand's `create` under
  the project-local name `createNitroStore`. Comments document the
  convention (one store per domain, subscribe to slices not the whole
  store).

First migration target
- src/components/navigator/views/navigatorRoomCreatorStore.ts (new):
  a Zustand store with `isCreating: boolean` and `beginCreate()` —
  the latter latches the flag to true, dispatches an internal
  setTimeout to auto-reset after 5s, and replaces any in-flight timer
  on re-entry. The timer handle lives in the store's closure, so a
  remount of the view doesn't reset the lockout and StrictMode's
  double-mount no longer schedules two pending timers.
- src/components/navigator/views/NavigatorRoomCreatorView.tsx:
  removes the two module-level `let` variables that the React Compiler
  was flagging ("Writing to a variable defined outside a component is
  not allowed"). The component now reads `isCreating` via a slice
  subscription and calls `beginCreate()` from the click handler. The
  imperative guard (`if (isCreating) return`) uses
  `useRoomCreatorStore.getState()` so it reads the latest value
  synchronously without being a stale closure.
- Also cleans up `FC<{}>` -> `FC` while touching the file.

Verification
- yarn eslint on the three touched files: 1 pre-existing error
  (the `setCategory(categories[0].id)` set-state-in-effect on the
  categories hook, deliberately left as-is in Phase C — it's the
  "init from late-arriving async data" pattern; baseline matches).
- yarn tsc: clean.

Migration path (per docs/ARCHITECTURE.md)
- This is the smallest possible Zustand pilot (~30 lines), chosen
  because the let-singleton anti-pattern was the most obvious quick
  win and the React Compiler was already complaining about it.
- Next adoption targets (cross-feature UI state): the toolbar's
  active-window state (currently inside scattered Contexts), the
  notification center's open-state, the catalog's currentPage/selection
  state (after the god-hook split).

https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
This commit is contained in:
simoleo89
2026-05-11 16:31:53 +00:00
parent 34b1b56788
commit fd1835ca5d
5 changed files with 66 additions and 52 deletions
@@ -5,13 +5,11 @@ import { GetClubMemberLevel, GetConfigurationValue, IRoomModel, LocalizeText, Se
import { Button, Flex, Grid, LayoutCurrencyIcon, LayoutGridItem, Text } from '../../../common';
import { useNavigator } from '../../../hooks';
import { NitroInput } from '../../../layout';
let isCreatingRoom = false;
let createRoomTimeout: ReturnType<typeof setTimeout> = null;
import { useRoomCreatorStore } from './navigatorRoomCreatorStore';
const MAX_VISITORS_LIST: number[] = Array.from({ length: 10 }, (_, i) => (i + 1) * 10);
export const NavigatorRoomCreatorView: FC<{}> = props =>
export const NavigatorRoomCreatorView: FC = () =>
{
const [ name, setName ] = useState<string>(null);
const [ description, setDescription ] = useState<string>(null);
@@ -25,7 +23,8 @@ export const NavigatorRoomCreatorView: FC<{}> = props =>
return (models && models.length) ? models[0].name : '';
});
const [ isCreating, setIsCreating ] = useState<boolean>(isCreatingRoom);
const isCreating = useRoomCreatorStore(s => s.isCreating);
const beginCreate = useRoomCreatorStore(s => s.beginCreate);
const { categories = null } = useNavigator();
const hcDisabled = GetConfigurationValue<boolean>('hc.disabled', false);
@@ -41,19 +40,11 @@ export const NavigatorRoomCreatorView: FC<{}> = props =>
const createRoom = () =>
{
if(isCreatingRoom) return;
if(useRoomCreatorStore.getState().isCreating) return;
isCreatingRoom = true;
setIsCreating(true);
beginCreate();
SendMessageComposer(new CreateFlatMessageComposer(name, description, 'model_' + selectedModelName, Number(category), Number(visitorsCount), tradesSetting));
if(createRoomTimeout) clearTimeout(createRoomTimeout);
createRoomTimeout = setTimeout(() =>
{
isCreatingRoom = false;
setIsCreating(false);
}, 5000);
};
useEffect(() =>