react-webview-architecture
Skill33 starsChanged 5 days agoMITView on GitHub
Architecture patterns for React-based webviews in the vscode-documentdb extension. Use when creating new webview components, modifying existing views (CollectionView, DocumentView), working with state management (Context API), integrating Fluent UI components, handling Monaco Editor or SlickGrid, solving stale closure bugs with refs, or debugging webview rendering issues. Does NOT cover tRPC messaging (see webview-trpc-messaging skill) or accessibility/ARIA (see accessibility-aria-expert skill).
In this repository17 other files it gives its agents.
- .github/copilot-instructions.mdCopilot instructions
- detecting-accessibility-issuesSkill
- backportSkill
- documentation-screenshotsSkill
- error-translationSkill
- flagging-fresh-dependenciesSkill
- fluentuiSkill
- leadSkill
- prepare-pull-requestSkill
- previewing-docs-siteSkill
- review-external-prSkill
- skill-creatorSkill
- telemetry-instrumentationSkill
- tree-cluster-architectureSkill
- ux-pr-reviewSkill
- webview-trpc-messagingSkill
- writing-release-notesSkill
The file
We split this skill into the rules it gives an agent
6 rules. About 0 are common, 3 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 27
Every webview boots through src/webviews/index.tsx:
- Distinctive: line 108
Always use functional updates: when state depends on previous value:
- Very specific: line 173
Use @fluentui/react-components (v9), themed via VSCodeFluentProvider:
- Distinctive: line 193
Avoid negative margins — fix layout with proper flexbox
- Very specific: line 244
Not calling
editor.layout(): after resize → blank Monaco panels - Distinctive: line 247
Inline styles: instead of SCSS files
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
React Webview Architecture
Patterns and conventions for React webviews in vscode-documentdb.
Related skills (do not duplicate):
- webview-trpc-messaging — tRPC routers, procedures, telemetry, AbortSignal, subscriptions, WebviewController
- accessibility-aria-expert — ARIA labels, Announcer, focus management, screen reader patterns
Full reference: See references/REACT_ARCHITECTURE_GUIDELINES.md
When to Use
- Creating or modifying a webview (DocumentView, CollectionView)
- Adding new components inside
src/webviews/ - Working with CollectionView context or state management
- Integrating Monaco Editor or SlickGrid
- Debugging stale closure issues in event handlers
Rendering Pipeline
Every webview boots through src/webviews/index.tsx:
root.render(
<VSCodeFluentProvider>
<WithWebviewContext vscodeApi={vscodeApi}>
<Component />
</WithWebviewContext>
</VSCodeFluentProvider>,
);VSCodeFluentProvider— adapts Fluent UI theming to VS Code's active color theme. Lives in@microsoft/vscode-ext-webview-fluentui, not insrc/; importing it also injects that package's Fluent overrides.WithWebviewContext— providesvscodeApi(postMessage) via React ContextWebviewRegistry— maps webview names → React components (in_integration/WebviewRegistry)
Configuration from the extension host is read via useConfiguration<T>().
File Organization
viewName/
├── ViewName.tsx # Main component
├── viewName.scss # Styles
├── viewNameContext.ts # Context + state types (if complex)
├── viewNameController.ts # WebviewController subclass (extension-side)
├── viewNameRouter.ts # tRPC router (extension-side, see webview-trpc-messaging skill)
├── constants.ts
├── components/ # Sub-components
├── hooks/ # Custom React hooks
├── types/ # TypeScript types
└── utils/ # HelpersComponent Hierarchy
DocumentView (simpler, good reference pattern):
DocumentView
├── ProgressBar (conditional: isLoading)
├── ToolbarDocuments
└── MonacoEditorCollectionView (complex, multi-tab):
CollectionView
├── ProgressBar (conditional)
├── ToolbarMainView
├── QueryEditor
│ └── MonacoAutoHeight (multiple: filter, project, sort)
├── TabList (Results | Query Insights [PREVIEW])
├── Results Tab:
│ ├── ToolbarViewNavigation + ToolbarDocumentManipulation + ViewSwitcher
│ ├── DataViewPanelTable / DataViewPanelTree / DataViewPanelJSON
│ └── ToolbarTableNavigation (Table View only)
└── Query Insights Tab:
└── QueryInsightsMain (3-stage progressive loading)State Management
Simple views (DocumentView): local useState + props
Complex views (CollectionView): React Context with [state, setState] tuple
export const CollectionViewContext = createContext<
[CollectionViewContextType, React.Dispatch<React.SetStateAction<CollectionViewContextType>>]
>([DefaultCollectionViewContext, () => {}]);
// Provider in parent
const [currentContext, setCurrentContext] = useState(DefaultCollectionViewContext);
<CollectionViewContext.Provider value={[currentContext, setCurrentContext]}>
// Consumer in child
const [currentContext, setCurrentContext] = useContext(CollectionViewContext);Always use functional updates when state depends on previous value:
setCurrentContext((prev) => ({
...prev,
isLoading: true,
activeQuery: { ...prev.activeQuery, pageNumber: 1 },
}));Stale Closure Pattern (CRITICAL)
Third-party components (SlickGrid) bind event handlers at initialization — they don't update when state changes. Always use refs to access current data in those handlers:
const dataRef = useRef(data);
useEffect(() => {
dataRef.current = data;
}, [data]);
const onCellDblClick = useCallback((event) => {
const item = dataRef.current[event.detail.args.row]; // ✅ always current
// NOT: data[event.detail.args.row]; ❌ stale closure
}, []); // stable deps onlyWhy: SlickGrid binds handlers once at init time. Without refs, handlers see the data from initialization, not the latest state. This caused multiple hard-to-debug issues.
Monaco Editor
Required patterns:
- Manual layout — Monaco doesn't auto-resize:
useEffect(() => {
const handler = debounce(() => editorRef.current?.layout(), 200);
window.addEventListener('resize', handler);
handleResize(); // initial layout
return () => window.removeEventListener('resize', handler);
}, []);- Dispose on unmount:
return () => {
editorRef.current?.dispose();
};- MonacoAutoHeight — self-sizing editor for query fields:
<MonacoAutoHeight
adaptiveHeight={{ enabled: true, maxLines: 10, minLines: 1, lineHeight: 19 }}
onExecuteRequest={() => onExecuteRequest()}
onMount={(editor, monaco) => handleEditorDidMount(editor, monaco)}
/>- JSON Schema delay — Monaco's JSON worker may not be ready immediately after mount. An AbortController-guarded delay is used (see QueryEditor for the pattern).
Fluent UI Integration
Use @fluentui/react-components (v9), themed via VSCodeFluentProvider:
| Component | Usage |
ProgressBar | Loading states |
Button, ToggleButton | Toolbar actions |
Tab, TabList | View switching |
Dropdown, Option | Selection (ViewSwitcher) |
Badge | Status/preview indicators |
MessageBar | Info/warning messages |
Skeleton, SkeletonItem | Loading placeholders |
Animations: Collapse from @fluentui/react-motion-components-preview
Styling
- Each component gets its own
.scssfile, imported directly - Shared styles in
sharedStyles.scss, applied via@extend - Consistent spacing unit:
10pxwith flexboxrow-gap/column-gap - No inline styles — move to SCSS files
- Avoid negative margins — fix layout with proper flexbox
.documentView {
display: flex;
flex-direction: column;
height: 100vh;
row-gap: 10px;
}Custom Hooks
| Hook | Purpose |
useSelectiveContextMenuPrevention() | Prevents browser context menu everywhere except Monaco editors. Call once in top-level view. |
useHideScrollbarsDuringResize() | Returns a function that temporarily hides scrollbars during layout transitions (500ms). |
Conditional Rendering Patterns
Object-based switch:
{{
'Table View': <DataViewPanelTable {...props} />,
'Tree View': <DataViewPanelTree {...props} />,
'JSON View': <DataViewPanelJSON {...props} />,
}[currentContext.currentView]}Loading State
const [isLoading, setIsLoading] = useState(false);
setIsLoading(true);
try {
await op();
} finally {
setIsLoading(false);
}
// In render:
{
isLoading && <ProgressBar thickness="large" shape="square" className="progressBar" />;
}Common Pitfalls
- Forgetting refs with third-party components → stale data in event handlers
- Not cleaning up event listeners, Monaco instances, AbortControllers in
useEffectreturn - Not calling
editor.layout()after resize → blank Monaco panels - Using
any→ use proper types orunknownwith type guards - Missing
l10n.t()on user-facing strings - Inline styles instead of SCSS files
- Negative margins to fix spacing — restructure layout with flexbox gap instead
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

