widgets: wrap each room + furniture widget in its own WidgetErrorBoundary

The umbrella boundary on RoomWidgetsView caught any widget crash but
unmounted every sibling along with the failing widget — a single bad
parser in ChatWidget would dark out the avatar info, chat input,
doorbell and all furniture overlays until the next remount.

Wraps each of the 13 direct children of RoomWidgetsView (AvatarInfo,
Chat, ChatInput, Doorbell, RoomTools, RoomFilterWords, RoomThumbnail,
FurniChooser, PetPackage, UserChooser, WordQuiz, FriendRequest, plus
the FurnitureWidgets umbrella) and each of the 20 sub-widgets inside
FurnitureWidgetsView in its own named WidgetErrorBoundary. A crash
now silently logs through NitroLogger with the widget name and
renders null for that one widget; every sibling keeps rendering.

The outer umbrella stays as defense-in-depth for the wrapper div and
the listener setup in RoomWidgetsView itself.

Closes the "Per-widget WidgetErrorBoundary wrapping" roadmap item;
updates CLAUDE.md and docs/ARCHITECTURE.md accordingly.
This commit is contained in:
simoleo89
2026-05-14 20:18:38 +02:00
parent 97c9717253
commit ab93113ce7
4 changed files with 43 additions and 51 deletions
+1 -1
View File
@@ -260,7 +260,7 @@ into `configurePreviewServer` so `yarn preview` keeps working.
| Zustand | `NavigatorRoomCreatorView` (`useRoomCreatorStore`) |
| 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`, `notification`, `friends`, `catalog` (three-way: `useCatalogData` / `useCatalogUiState` / `useCatalogActions` — all 48 consumers migrated, deprecated `useCatalog` shim removed) |
| `WidgetErrorBoundary` | `RoomWidgetsView` umbrella |
| `WidgetErrorBoundary` | `RoomWidgetsView` umbrella + per-widget wrap on all 13 room widgets and all 20 furniture widgets (so a crash in one widget no longer takes down its siblings) |
| Vitest | 162/162 cases — pure helpers + Zustand store + 2 component-/hook-level pilots (WidgetErrorBoundary, useDoorbellState) on top of the renderer-SDK mock at `tests/mocks/renderer-mock.ts`, 34 cases on the catalog pure helpers, 4 contract cases on the catalog filters |
| Form Actions | Login / Register / Forgot (LoginView.tsx) |
| Cherry-picked from `duckietm` PR #126 | `UserAccountSettingsView` (reset password / email / username under user settings), plus the wear-badge popup `canShowWearButton` gating |
+8 -17
View File
@@ -302,12 +302,12 @@ takes down the whole UI.
Implementation lives at `src/common/error-boundary/WidgetErrorBoundary.tsx`.
**Status.** Implemented + applied to `RoomWidgetsView` as the umbrella for
all in-room widgets. A widget crash now degrades gracefully (the offending
widget disappears) instead of unmounting the room.
A more granular pass could wrap each individual widget for finer-grained
fallbacks, but the umbrella alone already prevents the worst class of
failures.
all in-room widgets, **plus** a per-widget pass that wraps each of the 13
direct children of `RoomWidgetsView` and each of the 20 sub-widgets in
`FurnitureWidgetsView`. A crash in any single widget now silently logs
through `NitroLogger` and renders `null` for that widget only — its
siblings keep rendering. Each boundary carries a `name` prop matching
the widget so the log line identifies the culprit.
---
@@ -729,20 +729,11 @@ Remaining order of value/risk for the next contributor:
siblings under `src/hooks/catalog/`). Only after step 1 — React
Query removes ~60% of the file's responsibility, Zustand can absorb
the UI state slice.
3. **Per-widget `WidgetErrorBoundary` wrapping** inside `RoomWidgetsView`.
The umbrella is in place; granular wrapping means a crash in one
widget (e.g. `ChatWidgetView`) doesn't take down the rest of the
room overlay. Mechanical and safe.
4. **Hoist `WiredCreatorToolsView`'s shared state to a Zustand slice.**
3. **Hoist `WiredCreatorToolsView`'s shared state to a Zustand slice.**
The 4-tab split is done but the parent still passes ~25 props to
each tab. A slice at `src/components/wired-tools/wiredToolsStore.ts`
would make each tab subscribe to the keys it needs.
5. **Address the two open logic bugs** (see the "Known logic bugs"
section above): the `MainView` CREATED/ENDED race needs a session
token; the `LayoutFurniImageView` / `LayoutAvatarImageView` async
fetch race needs a request-id ref (or is solved by migrating the
image fetch to `useNitroQuery` keyed on props).
6. **Widen the component/hook Vitest coverage.** The renderer-SDK
4. **Widen the component/hook Vitest coverage.** The renderer-SDK
mock layer is in place (`tests/mocks/renderer-mock.ts`) and the
first two pilots — `WidgetErrorBoundary` and `useDoorbellState`
pass. Good follow-up targets: other `*State` hooks built on event
+13 -13
View File
@@ -161,20 +161,20 @@ export const RoomWidgetsView: FC<{}> = props =>
return (
<WidgetErrorBoundary name="RoomWidgets">
<div className="absolute top-0 left-0 pointer-events-none size-full">
<FurnitureWidgetsView />
<WidgetErrorBoundary name="FurnitureWidgets"><FurnitureWidgetsView /></WidgetErrorBoundary>
</div>
<AvatarInfoWidgetView />
<ChatWidgetView />
<ChatInputView />
<DoorbellWidgetView />
<RoomToolsWidgetView />
<RoomFilterWordsWidgetView />
<RoomThumbnailWidgetView />
<FurniChooserWidgetView />
<PetPackageWidgetView />
<UserChooserWidgetView />
<WordQuizWidgetView />
<FriendRequestWidgetView />
<WidgetErrorBoundary name="AvatarInfoWidget"><AvatarInfoWidgetView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="ChatWidget"><ChatWidgetView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="ChatInput"><ChatInputView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="DoorbellWidget"><DoorbellWidgetView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="RoomToolsWidget"><RoomToolsWidgetView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="RoomFilterWordsWidget"><RoomFilterWordsWidgetView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="RoomThumbnailWidget"><RoomThumbnailWidgetView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="FurniChooserWidget"><FurniChooserWidgetView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="PetPackageWidget"><PetPackageWidgetView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="UserChooserWidget"><UserChooserWidgetView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="WordQuizWidget"><WordQuizWidgetView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="FriendRequestWidget"><FriendRequestWidgetView /></WidgetErrorBoundary>
</WidgetErrorBoundary>
);
};
@@ -1,4 +1,5 @@
import { FC } from 'react';
import { WidgetErrorBoundary } from '../../../../common';
import { FurnitureBackgroundColorView } from './FurnitureBackgroundColorView';
import { FurnitureAreaHideView } from './FurnitureAreaHideView';
import { FurnitureBadgeDisplayView } from './FurnitureBadgeDisplayView';
@@ -24,26 +25,26 @@ export const FurnitureWidgetsView: FC<{}> = props =>
{
return (
<>
<FurnitureAreaHideView />
<FurnitureBackgroundColorView />
<FurnitureBadgeDisplayView />
<FurnitureCraftingView />
<FurnitureDimmerView />
<FurnitureExchangeCreditView />
<FurnitureExternalImageView />
<FurnitureFriendFurniView />
<FurnitureGiftOpeningView />
<FurnitureHighScoreView />
<FurnitureInternalLinkView />
<FurnitureMannequinView />
<FurniturePlaylistEditorWidgetView />
<FurnitureRoomLinkView />
<FurnitureSpamWallPostItView />
<FurnitureStackHeightView />
<FurnitureStickieView />
<FurnitureTrophyView />
<FurnitureContextMenuView />
<FurnitureYoutubeDisplayView />
<WidgetErrorBoundary name="FurnitureAreaHide"><FurnitureAreaHideView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="FurnitureBackgroundColor"><FurnitureBackgroundColorView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="FurnitureBadgeDisplay"><FurnitureBadgeDisplayView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="FurnitureCrafting"><FurnitureCraftingView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="FurnitureDimmer"><FurnitureDimmerView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="FurnitureExchangeCredit"><FurnitureExchangeCreditView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="FurnitureExternalImage"><FurnitureExternalImageView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="FurnitureFriendFurni"><FurnitureFriendFurniView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="FurnitureGiftOpening"><FurnitureGiftOpeningView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="FurnitureHighScore"><FurnitureHighScoreView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="FurnitureInternalLink"><FurnitureInternalLinkView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="FurnitureMannequin"><FurnitureMannequinView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="FurniturePlaylistEditorWidget"><FurniturePlaylistEditorWidgetView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="FurnitureRoomLink"><FurnitureRoomLinkView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="FurnitureSpamWallPostIt"><FurnitureSpamWallPostItView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="FurnitureStackHeight"><FurnitureStackHeightView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="FurnitureStickie"><FurnitureStickieView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="FurnitureTrophy"><FurnitureTrophyView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="FurnitureContextMenu"><FurnitureContextMenuView /></WidgetErrorBoundary>
<WidgetErrorBoundary name="FurnitureYoutubeDisplay"><FurnitureYoutubeDisplayView /></WidgetErrorBoundary>
</>
);
};