Files
Nitro-V3/CLAUDE.md
T
simoleo89 f75762a2db Add CLAUDE.md + refresh docs/ARCHITECTURE.md to current state
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.
2026-05-11 18:05:12 +00:00

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-dom 19.2.5, @types/react 19.2.x.
  • TypeScript: TS 6 for build, TS 7 native preview (@typescript/native-preview, invoked via tsgo) for the typecheck script.
  • Vite 8 + @vitejs/plugin-react 6 + babel-plugin-react-compiler 1.0.
  • ESLint 10 + typescript-eslint 8 + 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-boundary 6.

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 values
  • use<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 between RoomSessionEvent.CREATED and ENDED (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. Prefer feat/<description>.
  • Never merge a branch that violates the layout convention above. The feat/react19-hooks-adapter branch (deleted) put hooks under src/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 test must stay green on every commit. Currently 77/77.
  • Lint baseline: don't regress. Some pre-existing errors (FC<{}>, IMessageEvent | undefined redundant 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 from vite.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/