mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
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:
+2
-1
@@ -29,7 +29,8 @@
|
||||
"react-error-boundary": "^6.1.1",
|
||||
"react-icons": "^5.6.0",
|
||||
"react-player": "^2.16.0",
|
||||
"use-between": "^1.4.0"
|
||||
"use-between": "^1.4.0",
|
||||
"zustand": "^5.0.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { createNitroStore } from '../../../state/createNitroStore';
|
||||
|
||||
const CREATE_LOCKOUT_MS = 5000;
|
||||
|
||||
interface RoomCreatorState
|
||||
{
|
||||
isCreating: boolean;
|
||||
/**
|
||||
* Latch `isCreating` to true and auto-clear after CREATE_LOCKOUT_MS.
|
||||
* Acts as a debounce so duplicate clicks on the Create button can't
|
||||
* dispatch the composer twice.
|
||||
*/
|
||||
beginCreate: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the previous `let isCreatingRoom` / `let createRoomTimeout`
|
||||
* module-level pair (anti-pattern flagged by the React Compiler:
|
||||
* "Writing to a variable defined outside a component or hook").
|
||||
*
|
||||
* The timer handle lives in the store's closure so a remount of the
|
||||
* view doesn't reset the in-flight lockout, and so React strict-mode's
|
||||
* double-mount no longer schedules two pending timers.
|
||||
*/
|
||||
export const useRoomCreatorStore = createNitroStore<RoomCreatorState>()((set) =>
|
||||
{
|
||||
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
return {
|
||||
isCreating: false,
|
||||
beginCreate: () =>
|
||||
{
|
||||
if(timeoutHandle !== null) clearTimeout(timeoutHandle);
|
||||
|
||||
set({ isCreating: true });
|
||||
|
||||
timeoutHandle = setTimeout(() =>
|
||||
{
|
||||
timeoutHandle = null;
|
||||
set({ isCreating: false });
|
||||
}, CREATE_LOCKOUT_MS);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -1,42 +1,15 @@
|
||||
/**
|
||||
* Skeleton for proposal #5 (unified UI store).
|
||||
* Re-export of zustand's `create` under a project-local name.
|
||||
*
|
||||
* NOT YET ENABLED — `zustand` is not in package.json.
|
||||
* To activate:
|
||||
* Convention: each domain owns one store file. Either:
|
||||
* - `src/state/<domain>.ts` for cross-feature stores
|
||||
* - `src/components/<area>/<feature>Store.ts` for feature-local stores
|
||||
*
|
||||
* yarn add zustand
|
||||
* Components subscribe to specific slices only:
|
||||
*
|
||||
* Then this file becomes:
|
||||
* const isCreating = useNavigatorRoomCreatorStore(s => s.isCreating);
|
||||
*
|
||||
* import { create } from 'zustand';
|
||||
* export const createNitroStore = create;
|
||||
*
|
||||
* The naming convention below documents the intended structure: each
|
||||
* feature owns one slice file under `src/features/<feature>/state/`,
|
||||
* importing `createNitroStore` from here.
|
||||
*
|
||||
* Example slice (to be created when zustand is installed):
|
||||
*
|
||||
* // src/features/wired-tools/state/wiredToolsSlice.ts
|
||||
* import { createNitroStore } from '../../../state/createNitroStore';
|
||||
*
|
||||
* type WiredToolsState = {
|
||||
* activeTab: 'monitor' | 'variables' | 'inspection' | 'chests' | 'settings';
|
||||
* setActiveTab: (tab: WiredToolsState['activeTab']) => void;
|
||||
* };
|
||||
*
|
||||
* export const useWiredToolsStore = createNitroStore<WiredToolsState>()((set) => ({
|
||||
* activeTab: 'monitor',
|
||||
* setActiveTab: (tab) => set({ activeTab: tab }),
|
||||
* }));
|
||||
*
|
||||
* First migration target suggested in docs/ARCHITECTURE.md is the
|
||||
* `let isCreatingRoom = false` / `createRoomTimeout` singleton pair in
|
||||
* NavigatorRoomCreatorView.tsx — a ~5-line conversion that removes a
|
||||
* react-compiler/react-compiler "writing outside component" violation.
|
||||
* Do NOT pull the whole store (`const all = useStore()`) — that
|
||||
* subscribes to every change and defeats the point.
|
||||
*/
|
||||
|
||||
export const createNitroStore = (): never =>
|
||||
{
|
||||
throw new Error('createNitroStore is not enabled. See docs/ARCHITECTURE.md proposal #5.');
|
||||
};
|
||||
export { create as createNitroStore } from 'zustand';
|
||||
|
||||
@@ -3221,3 +3221,8 @@ zod@^3.22.4:
|
||||
version "4.4.3"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-4.4.3.tgz#b680f172885d18bbebf21a834ea25e55a1bbf356"
|
||||
integrity sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==
|
||||
|
||||
zustand@^5.0.13:
|
||||
version "5.0.13"
|
||||
resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.13.tgz#06995c126e8903cd27100af04da91c36ae3051ed"
|
||||
integrity sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==
|
||||
|
||||
Reference in New Issue
Block a user