docs: comprehensive refresh after the React 19 modernization round

Three top-level files brought in sync with the work landed on
feat/react19-modernization:

- CHANGELOG.md gets a 'React 19 Modernization Phase 2 (2026-05-12)'
  section spanning all four pattern groups (event-state companions,
  TanStack queries on the catalog layer, god-hook splits in the
  doorbell + singleton-filter styles, Pixi v8 / TS 5.7+ alignment),
  the Vitest growth 65 -> 113, and the in-scope logic bug fixes.
- ARCHITECTURE.md bumps the test ledger 99 -> 113 (adds the
  avatar-info reducer suite), documents the new pure-module test
  convention (concrete file paths + 'import type' for renderer
  event types), and lists the two new singleton-filter splits
  (notification, friends).
- CLAUDE.md mirrors the same updates plus a 'Singleton-filter split'
  recipe alongside the doorbell-style one; useNitroEventInvalidator
  is documented next to useNitroQuery; the 'What's wired up' table
  enumerates all 10 split hooks. Test count bumped 99 -> 113 in
  both the 'Vitest' row and the green-bar house rule.
This commit is contained in:
simoleo89
2026-05-11 23:13:56 +02:00
parent 3c732f1c1a
commit cc225bdc5d
3 changed files with 184 additions and 7 deletions
+121
View File
@@ -1,5 +1,126 @@
# Changelog # Changelog
## React 19 Modernization Phase 2 (2026-05-12)
Long-running work on the `feat/react19-modernization` branch — see
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the design rationale.
Companion changes shipped on `feat/react19-event-bus` in
[`Nitro_Render_V3`](../Nitro_Render_V3) — see that repo's CLAUDE.md
for the renderer-side notes.
### Pattern #1: `useNitroEventState` + companions
- New `useNitroEventReducer` / `useMessageEventReducer` for the case
where multiple event types collapse into one owned state slice.
- New `useExternalSnapshot` — typed wrapper of
`useSyncExternalStore` pairing the renderer's
`EventDispatcher.subscribe()` with `getXxxSnapshot()` getters.
- Pilot adoption: `useAvatarInfoWidget` now owns the figure / badges /
group merge (three event listeners moved out of
`InfoStandWidgetUserView`, three `CloneObject` calls dropped).
Reducers extracted to `src/hooks/rooms/widgets/avatarInfo.reducers.ts`
with 14 Vitest cases.
- `useInventoryFurni` refactored to call three pure reducers
(`useInventoryFurni.reducers.ts`) instead of inlining ~250 LOC of
merge logic in the event handlers. Module-level
`furniMsgFragments` becomes a `useRef` — eliminates a latent bug
where two simultaneous client instances would have trampled each
other's fragment buffers. Empty `FurniturePostItPlacedEvent` listener
dropped.
### Pattern #2: `useNitroQuery` adoption
- New `useNitroEventInvalidator(eventType, queryKey, accept?)` companion
in `src/api/nitro-query/` — invalidates a query slot every time the
renderer pushes the matching parser event. Required when the server
refreshes data outside the request cycle (e.g. ClubGiftInfoEvent
after a gift claim).
- Seven catalog fetches lifted out of `useCatalog` into dedicated
TanStack queries:
- `useGiftConfiguration` (GiftWrappingConfigurationEvent)
- `useUserGroups` — consolidates 5 sites that each dispatched
`CatalogGroupsComposer` independently
- `useClubOffers(windowId)` — per-windowId, with `accept` filter
- `useSellablePetPalette(breed)` — per-breed, with `accept` filter
- `useMarketplaceConfiguration` — lifted out of a self-fetch in
`MarketplacePostOfferView`
- `useClubGifts` — paired with `useNitroEventInvalidator` for the
server-push-after-SelectClubGift case
- `ICatalogOptions` (the "catalogOptions" bag that the various views
stuffed their fetched data into) is now **empty and deleted**.
### Pattern #4: god-hook splits
Five new splits in this round, two patterns. The doorbell-style
(state + actions + shim, no shared singleton) for hooks whose actions
are pure-dispatch:
- **chat-input** (334 LOC → 3 files) — `useChatInputState` owns the
5 state slices + 3 event listeners + 3 lifecycle effects;
`useChatInputActions` owns `sendChat` with the full slash-command
repertoire and the outgoing-translation pipeline. Single consumer
(`ChatInputView`) keeps the original tuple via the shim.
The `useBetween` singleton-filter style for hooks where actions
mutate shared state:
- **wired-tools** (618 LOC) — 20 consumers; `useWiredToolsStore`
internal singleton, public `useWiredToolsState` /
`useWiredToolsActions` filter views, `useWiredTools` shim.
- **translation** (600 LOC) — 6 consumers; `useTranslationStore`
inline + filter views.
- **notification** (493 LOC) — ~44 consumers, most of which use a
single action (`simpleAlert` or `showConfirm`); the read-only state
slice exposes the three queue arrays for the renderer view layer.
- **friends** (258 LOC) — 16 consumers; state slice covers the friend
list / settings / derived online-offline split, actions slice covers
`requestFriend` / `requestResponse` / `followFriend` /
`updateRelationship`.
Documented skip-motivated splits: `useChatWidget`,
`useChatCommandSelector`, `useFurniturePresentWidget`,
`useAvatarInfoWidget`, `useNavigator`, `useMessenger`,
`usePetPackageWidget`, `useWordQuizWidget`. Reasons logged in commit
messages.
### Typecheck / Pixi v8 / Arcturus alignment
- Repository-wide `tsgo` (TS 7 preview) error count: **134 → 0** client,
**24 → 0** renderer. Notable clusters: framer-motion `Variants`
typing on Toolbar + FriendsBar (-33), `useFurniChooserState`
retyped as `IRoomObject` + dead `getUserData` guard dropped (-10),
React 19 `useRef<T>()``useRef<T>(null)` sweep on 15 sites (-15),
`IGetImageListener` single-arg signature migration on 3 sites,
`ColorVariantType` extended with the 5 `outline-*` bootstrap
variants.
- Renderer-side aligned with Pixi v8 (Filter[] narrowing,
WebGLRenderer narrowing, ImageLike cast) and TS 5.7+ ArrayBuffer
drift (BinaryReader / BinaryWriter / WsSessionCrypto / NitroBundle).
- Cross-repo additions on `Nitro_Render_V3`:
`RoomEnterComposer` now accepts optional `spawnX`/`spawnY` matching
Arcturus' `RequestRoomLoadEvent` optional tail; `RoomSettingsData`
surfaces the `allowUnderpass` field that Arcturus already emits.
Dead `sendWhisperGroupMessage` / `ChatWhisperGroupComposer`
reference removed.
### Vitest coverage
Bumped from 65 → 113 cases across 8 test files. New coverage:
- `dedupeBadges.test.ts` (6) — slot-preserving badge dedup.
- `catalog-favorites.helpers.test.ts` (16) — v2→v3 localStorage
migration + per-catalog-type storage-key routing.
- `avatar-info-reducers.test.ts` (14) — three reducer bail-out
branches + apply paths.
- `friendly-time.test.ts` (12) — `FriendlyTime` with a deterministic
`LocalizeText` mock.
### Logic bug fixes (in scope)
- `useInventoryFurni`'s module-level `furniMsgFragments` buffer
scoped to `useRef`.
- `RoomChatHandler.dispatchEvent(RoomSessionChatEvent)` arg order
fix in renderer — `chatColours` and `links` slots were swapped.
- `PetBreedingMessageParser.bytesAvailable < 12` was a boolean-vs-number
bug; replaced with the standard guard pattern.
- `useOnClickChat` was passing an extra 8th arg to `showConfirm`
(signature only takes 7).
- `UserContainerView` was passing `userProfile.friendsCount` (number)
to a `LocalizeText` placeholder array (expects string).
## Badge System Rework (2026-04-04) ## Badge System Rework (2026-04-04)
### Bug Fixes ### Bug Fixes
+43 -5
View File
@@ -109,8 +109,46 @@ const { data } = useNitroQuery<SomeParser, SomeData>({
``` ```
Already wired up; `QueryClientProvider` is mounted in `src/index.tsx`. Already wired up; `QueryClientProvider` is mounted in `src/index.tsx`.
Adopted on `OfferView`, `CatalogLayoutRoomAdsView`, `ModToolsChatlogView`,
`CfhChatlogView`. Companion `useNitroEventInvalidator(eventType, queryKey, accept?)`
import from `src/api/nitro-query`. Subscribes to the renderer event
and invalidates the query slot on every push, so server-driven
refresh paths work the same as the initial request/response (e.g.
ClubGiftInfoEvent firing again after the user claims a gift).
### Singleton-filter split for `useBetween`-based hooks
When a hook backs many consumers but most only need either state OR
actions (not both), split it without breaking the shared-singleton
guarantee:
```ts
// internal: state + actions in one closure
const useFooStore = () => {
const [ data, setData ] = useState(...);
// listeners, effects, actions ...
return { data, doThing };
};
// public: read-only filter
export const useFooState = () => {
const { data } = useBetween(useFooStore);
return { data };
};
// public: imperative filter
export const useFooActions = () => {
const { doThing } = useBetween(useFooStore);
return { doThing };
};
// deprecated shim — keeps the historical return shape
export const useFoo = () => useBetween(useFooStore);
```
`useBetween` ensures all three entry points hit the same store
instance, so listeners/effects register once. Used by `useWiredTools`,
`useTranslation`, `useNotification`, `useFriends`.
### Zustand stores ### Zustand stores
@@ -159,9 +197,9 @@ Login / Register / Forgot in `src/components/login/LoginView.tsx` use
| `useNitroQuery` + `useNitroEventInvalidator` | `OfferView`, `CatalogLayoutRoomAdsView`, `ModToolsChatlogView`, `CfhChatlogView`, `useGiftConfiguration`, `useUserGroups`, `useClubOffers(windowId)`, `useSellablePetPalette(breed)`, `useMarketplaceConfiguration`, `useClubGifts` (with invalidator) | | `useNitroQuery` + `useNitroEventInvalidator` | `OfferView`, `CatalogLayoutRoomAdsView`, `ModToolsChatlogView`, `CfhChatlogView`, `useGiftConfiguration`, `useUserGroups`, `useClubOffers(windowId)`, `useSellablePetPalette(breed)`, `useMarketplaceConfiguration`, `useClubGifts` (with invalidator) |
| Zustand | `NavigatorRoomCreatorView` (`useRoomCreatorStore`) | | Zustand | `NavigatorRoomCreatorView` (`useRoomCreatorStore`) |
| God-hook split (state + actions + shim) | `doorbell`, `poll`, `furni-chooser`, `user-chooser`, `friend-request`, `chat-input` | | God-hook split (state + actions + shim) | `doorbell`, `poll`, `furni-chooser`, `user-chooser`, `friend-request`, `chat-input` |
| God-hook split (`useBetween` singleton + state filter + actions filter + shim) | `wired-tools`, `translation` | | God-hook split (`useBetween` singleton + state filter + actions filter + shim) | `wired-tools`, `translation`, `notification`, `friends` |
| `WidgetErrorBoundary` | `RoomWidgetsView` umbrella | | `WidgetErrorBoundary` | `RoomWidgetsView` umbrella |
| Vitest | 99/99 cases on pure helpers + the Zustand store | | Vitest | 113/113 cases on pure helpers + the Zustand store |
| Not yet | Notes | | Not yet | Notes |
|---|---| |---|---|
@@ -197,7 +235,7 @@ Fix shapes documented; both are reasonable PRs on their own.
- **Skip-motivated god-hook splits are fine** — when a hook's actions - **Skip-motivated god-hook splits are fine** — when a hook's actions
mutate internal state, document the reason in the commit message and mutate internal state, document the reason in the commit message and
move on rather than forcing a bad split. move on rather than forcing a bad split.
- **`yarn test` must stay green** on every commit. Currently 99/99. - **`yarn test` must stay green** on every commit. Currently 113/113.
- **Lint baseline**: don't regress. Some pre-existing errors (`FC<{}>`, - **Lint baseline**: don't regress. Some pre-existing errors (`FC<{}>`,
`IMessageEvent | undefined` redundant union in the local sandbox where `IMessageEvent | undefined` redundant union in the local sandbox where
the renderer SDK isn't installed) are out of scope here. the renderer SDK isn't installed) are out of scope here.
+20 -2
View File
@@ -427,6 +427,18 @@ The current branch (**`feat/react19-modernization`**, PR #2) has applied:
Wired tools — six consumers split across read-only views Wired tools — six consumers split across read-only views
(settings panel, bootstrap) and dispatch sites (messenger, chat (settings panel, bootstrap) and dispatch sites (messenger, chat
input). input).
- **notification**: `useNotificationStore` (internal singleton) +
`useNotificationState` (queue arrays for the renderer view) +
`useNotificationActions` (8 entry points: simpleAlert,
showNitroAlert, showTradeAlert, showConfirm, showSingleBubble,
closeAlert, closeBubbleAlert, closeConfirm) + shim. The ~30
message-event listeners and 5 state slices stay in the singleton.
Used by ~44 consumers, most of which only need one action.
- **friends**: `useFriendsStore` (internal singleton) +
`useFriendsState` (friends arrays, settings, derived
online/offline split, lookup helpers) + `useFriendsActions`
(requestFriend, requestResponse, followFriend, updateRelationship)
+ shim. 16 consumers.
- **Zustand** (proposal #5) — **enabled**. `zustand` installed; factory at - **Zustand** (proposal #5) — **enabled**. `zustand` installed; factory at
`src/state/createNitroStore.ts`. First adoption: the `let isCreatingRoom` `src/state/createNitroStore.ts`. First adoption: the `let isCreatingRoom`
/ `createRoomTimeout` module-level pair in `NavigatorRoomCreatorView` / `createRoomTimeout` module-level pair in `NavigatorRoomCreatorView`
@@ -472,7 +484,7 @@ Status after this round of work:
- Vitest 3 + jsdom + `@testing-library/react` + `@testing-library/jest-dom` - Vitest 3 + jsdom + `@testing-library/react` + `@testing-library/jest-dom`
configured. Separate `vitest.config.mts` so the runner doesn't drag in configured. Separate `vitest.config.mts` so the runner doesn't drag in
the renderer SDK aliases from `vite.config.mjs`. the renderer SDK aliases from `vite.config.mjs`.
- **99 cases passing** across 7 test files: - **113 cases passing** across 8 test files:
- `WiredCreatorTools.helpers.test.ts` (18) — formatters + snapshot - `WiredCreatorTools.helpers.test.ts` (18) — formatters + snapshot
factory. factory.
- `navigatorRoomCreatorStore.test.ts` (4) — Zustand store invariants - `navigatorRoomCreatorStore.test.ts` (4) — Zustand store invariants
@@ -488,10 +500,16 @@ Status after this round of work:
(covers the helper used by the InfoStand pilot). (covers the helper used by the InfoStand pilot).
- `catalog-favorites.helpers.test.ts` (16) — localStorage parse + - `catalog-favorites.helpers.test.ts` (16) — localStorage parse +
v2→v3 migration + per-catalog-type storage-key routing. v2→v3 migration + per-catalog-type storage-key routing.
- `avatar-info-reducers.test.ts` (14) — InfoStand reducer pilot:
bail-out branches (state-not-AvatarInfoUser, mismatched
user/roomIndex, equal-after-dedup) + the figure / favorite-group
apply paths.
- **Pure-module convention**: tests live in `tests/` and import from - **Pure-module convention**: tests live in `tests/` and import from
concrete file paths (e.g. `../src/api/catalog/CatalogType`) rather concrete file paths (e.g. `../src/api/catalog/CatalogType`) rather
than the api barrel, so jsdom doesn't transitively load the renderer than the api barrel, so jsdom doesn't transitively load the renderer
SDK's Pixi-bound modules. SDK's Pixi-bound modules. Renderer event type imports use
`import type { … }` so they're erased at compile time and don't
trigger the runtime module load either.
- `yarn test` + `yarn test:watch` scripts added. - `yarn test` + `yarn test:watch` scripts added.
### Logic bug fixes ### Logic bug fixes