Two doc changes so a fresh local Claude Code session can pick up the
branch without re-discovering the conventions and the work-in-progress.
CLAUDE.md (new, repo root)
- Onboarding file Claude Code reads automatically at session start.
- TL;DR with branch name + PR number, points at docs/ARCHITECTURE.md.
- Stack snapshot (React 19, TS 7 native, Vite 8 + Compiler, Zustand 5,
TanStack Query 5, Vitest 3).
- Layout convention spelled out — `src/components/<area>/<feature>/`
for views, `src/hooks/<area>/<feature?>/` flat for hooks. The
rejected-feature-folders decision is the most stepped-on rake, so
it lives here at the top.
- The canonical 3-file god-hook split shape with doorbell as the
reference.
- Patterns to use with copy-pasteable signatures: useNitroEventState,
useMessageEventState, useNitroQuery (with the accept() filter),
Zustand stores via createNitroStore, WidgetErrorBoundary.
- "Wired up vs not yet" matrix: what each pattern is adopted on and
what the next reasonable target is.
- Pointer to the two still-open logic bugs (MainView CREATED/ENDED
race; LayoutFurniImageView async fetch race) with fix shapes.
- House rules: commit author override, no claude/... branch names,
never merge a layout-violating branch, skip-motivated splits are
fine if explained in the commit message.
docs/ARCHITECTURE.md (refresh)
- "What's already in place" rewritten to reflect the full state of
the feat/react19-modernization branch:
* stale references to the old claude/update-react-typescript-He2rs
branch removed
* the three additional god-hook splits done since the last edit
(furni-chooser, user-chooser, friend-request) added
* the 4 useNitroQuery migration sites listed (OfferView,
CatalogLayoutRoomAdsView, ModToolsChatlogView, CfhChatlogView)
* the three additional WiredCreatorToolsView tab extractions
(Monitor, Inspection, Variables) with the 4493 -> 3544 line
counter
* dead-code removal of the legacy login dialogs documented
* the Vitest count updated from 22 to 77 across 6 test files
* usePollSubscriptions hoist to RoomWidgetsView noted
- "How to pick the next refactor PR" rewritten:
* completed items removed (the previous list still had
"hoist usePollSubscriptions" as todo even though it's done,
and "per-tab WiredCreatorTools split" same)
* remaining priorities re-ordered: useCatalog migration (1),
useCatalog split (2), per-widget error boundaries (3),
wired-tools shared-state Zustand slice (4), the two open
logic bugs (5), wider Vitest coverage (6).
* "skipped intentionally" subsection added for the god-hook
splits that need design work first (pet-package, word-quiz,
chat-input, chat-widget, avatar-info).
Verification
- yarn test: 77/77 still passing.
- grep claude/update-react-typescript-He2rs docs/ARCHITECTURE.md: 0
(no stale branch refs).
Now a fresh `claude` session in this repo can read CLAUDE.md, follow
the link to ARCHITECTURE.md, and start contributing without re-asking
the conventions.
8.5 KiB
Claude Code — project memory for Nitro-V3
This file is read automatically by Claude Code at session start. It captures the conventions and current state of this branch so a new session can hit the ground running.
TL;DR
This branch — feat/react19-modernization — is a long-running modernization
of the Nitro V3 client: bump to React 19.2 idioms, add the supporting
infrastructure (TanStack Query, Zustand, Vitest, React Compiler, error
boundaries), split a few god-hooks, and audit logic bugs along the way.
PR is #2 on simoleo89/Nitro-V3.
Detailed status, decisions, and next steps live in docs/ARCHITECTURE.md —
read that before starting anything non-trivial.
Commands
| Goal | Command |
|---|---|
| Dev server | yarn start |
| Production build | yarn build |
| Lint | yarn eslint |
| Type-check (TS 7 native, fast) | yarn typecheck |
| Test (Vitest, once) | yarn test |
| Test (watch) | yarn test:watch |
The renderer SDK (@nitrots/nitro-renderer) is consumed via a filesystem
link to a sibling working tree — ../Nitro_Render_V3 (preferred) or
../renderer (legacy). Without it, yarn typecheck reports TS2307 across
the codebase — that's expected on a sandbox without the renderer, not a
regression.
Stack snapshot
- React 19.2.5,
react-dom19.2.5,@types/react19.2.x. - TypeScript: TS 6 for build, TS 7 native preview (
@typescript/native-preview, invoked viatsgo) for thetypecheckscript. - Vite 8 +
@vitejs/plugin-react6 +babel-plugin-react-compiler1.0. - ESLint 10 +
typescript-eslint8 +eslint-plugin-react-hooks@7+eslint-plugin-react-compiler. - TanStack Query 5 (
@tanstack/react-query+ devtools). - Zustand 5.
- Vitest 3 + jsdom +
@testing-library/react+@testing-library/jest-dom. react-error-boundary6.
Layout convention (DO NOT CHANGE)
Established by the team and recorded in docs/ARCHITECTURE.md proposal #3
(rejected the src/features/ alternative). Stay on this layout — every PR
that violates it will need to be reworked.
src/components/<area>/<feature>/ → views (.tsx only)
e.g. src/components/room/widgets/doorbell/DoorbellWidgetView.tsx
src/hooks/<area>/<feature?>/ → hooks, FLAT files, no per-feature subfolder
e.g. src/hooks/rooms/widgets/useDoorbellState.ts
src/hooks/rooms/widgets/useDoorbellActions.ts
src/hooks/rooms/widgets/useDoorbellWidget.ts (deprecated shim)
src/api/ → cross-cutting helpers (LocalizeText, composers, formatters)
src/common/ → reusable UI primitives + error boundary
src/state/ → Zustand stores (cross-feature only)
tests/ → Vitest suites (mirror filename of subject)
When splitting a god-hook the convention is 3 files, all flat in the hooks barrel directory:
use<Feature>State.ts— state + event subscriptions + derived valuesuse<Feature>Actions.ts— pure imperative actions (no state writes)use<Feature>Widget.ts— deprecated wrapper that composes the two and preserves the old return shape so existing consumers don't break
See useDoorbellState/useDoorbellActions/useDoorbellWidget as the
canonical pattern.
Patterns to use
useNitroEventState / useMessageEventState
For "derived state from a single event" replace the two-step
useState + useNitroEvent(e => setState(...)) with a single call:
const foo = useNitroEventState(SomeEvent, e => e.payload, initial);
const data = useMessageEventState(SomeParser, e => e.getParser()?.field ?? null, null);
The selector is held in a useLayoutEffect-refreshed ref so the listener
stays registered across renders. Both hooks are exported from
src/hooks/events.
useNitroQuery
For composer/parser request-response pairs:
const { data } = useNitroQuery<SomeParser, SomeData>({
key: ['nitro', 'domain', 'request', ...args],
request: () => new SomeComposer(args),
parser: SomeParser,
select: e => e.getParser()?.data,
accept: e => e.getParser()?.correlationKey === args, // optional, for shared event bus
staleTime: 60_000,
});
Already wired up; QueryClientProvider is mounted in src/index.tsx.
Adopted on OfferView, CatalogLayoutRoomAdsView, ModToolsChatlogView,
CfhChatlogView.
Zustand stores
For cross-feature UI state (avoid module-level let):
import { createNitroStore } from '@/state/createNitroStore';
export const useFooStore = createNitroStore<FooState>()((set) => ({
...
}));
Components subscribe to slices, not the whole store:
const value = useFooStore(s => s.value);
First adoption: src/components/navigator/views/navigatorRoomCreatorStore.ts.
WidgetErrorBoundary
Wrap any in-room widget tree so a crash degrades gracefully (logs to
NitroLogger, falls back to null). Already applied at RoomWidgetsView
as an umbrella; per-widget wrapping is a follow-up.
<WidgetErrorBoundary name="ChatWidget">
<ChatWidgetView />
</WidgetErrorBoundary>
Form Actions
Login / Register / Forgot in src/components/login/LoginView.tsx use
useActionState + useFormStatus. The legacy non-Action versions in
src/components/login/components/{Register,Forgot}Dialog.tsx and
shared.ts have been removed (dead code).
What's wired up and what isn't
| Adopted | Pilot sites |
|---|---|
useNitroEventState |
OfferView |
useNitroQuery |
OfferView, CatalogLayoutRoomAdsView, ModToolsChatlogView, CfhChatlogView |
| Zustand | NavigatorRoomCreatorView (useRoomCreatorStore) |
| God-hook split | doorbell, poll, furni-chooser, user-chooser, friend-request |
WidgetErrorBoundary |
RoomWidgetsView umbrella |
| Vitest | 77/77 cases on pure helpers + the Zustand store |
| Not yet | Notes |
|---|---|
Split useCatalog (~1100 LOC) |
Migrate read-only fetches to useNitroQuery first, then split into useCatalogData / useCatalogUiState / useCatalogActions. |
Split useChatInputWidget / useChatWidget / useAvatarInfoWidget |
Large state machines; needs careful per-file design before mechanical split. |
Split usePetPackageWidget / useWordQuizWidget |
Their "actions" mutate internal state; need to either pass args or hoist state to a store first. Documented in commit messages, skipped intentionally. |
| Hoist Wired Creator Tools shared state to a Zustand slice | Would remove ~25 props passed to the 3 tab sub-components. |
| Wider Vitest coverage (React components) | @testing-library/* is installed; needs a small renderer-SDK mock layer first. |
Known open logic bugs
Read docs/ARCHITECTURE.md "Known logic bugs" section. The two still-open
ones:
MainView.tsx:47-48— race betweenRoomSessionEvent.CREATEDandENDED(no session token guard).LayoutFurniImageView/LayoutAvatarImageView— async fetch race when props change twice in quick succession.
Fix shapes documented; both are reasonable PRs on their own.
House rules
- Commit author:
simoleo89 <simoleo89@users.noreply.github.com>. When committing, pass these via per-command overrides (git -c user.name=simoleo89 -c user.email=...) — do NOT modify the global git config. - No
claude/...branch names — auto-generated names should be renamed before pushing. Preferfeat/<description>. - Never merge a branch that violates the layout convention above.
The
feat/react19-hooks-adapterbranch (deleted) put hooks undersrc/components/...; that's wrong and a recurring temptation. - Skip-motivated god-hook splits are fine — when a hook's actions mutate internal state, document the reason in the commit message and move on rather than forcing a bad split.
yarn testmust stay green on every commit. Currently 77/77.- Lint baseline: don't regress. Some pre-existing errors (
FC<{}>,IMessageEvent | undefinedredundant union in the local sandbox where the renderer SDK isn't installed) are out of scope here.
Where everything lives
- Architecture doc:
docs/ARCHITECTURE.md - Test runner config:
vitest.config.mts(separate fromvite.config.mjs) - Test setup:
tests/setup.ts - React Query adapter:
src/api/nitro-query/createNitroQuery.ts - Zustand factory:
src/state/createNitroStore.ts - Error boundary:
src/common/error-boundary/WidgetErrorBoundary.tsx - Event hooks (
useNitroEvent,useMessageEvent,useNitroEventState,useMessageEventState):src/hooks/events/ - Wired-tools split (types/constants/helpers + 3 tab views):
src/components/wired-tools/