microsoft/react-webview-architecture
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).
npx skills add https://github.com/microsoft/vscode-documentdb --skill react-webview-architecture
Patterns and conventions for React webviews in vscode-documentdb.
Related skills (do not duplicate):
Full reference: See references/REACT_ARCHITECTURE_GUIDELINES.md
src/webviews/Every webview boots through src/webviews/index.tsx:
root.render(
<DynamicThemeProvider useAdaptive={true}>
<WithWebviewContext vscodeApi={vscodeApi}>
<Component />
</WithWebviewContext>
</DynamicThemeProvider>,
);
DynamicThemeProvider — adapts Fluent UI theming to VS Code's active color themeWithWebviewContext — provides vscodeApi (postMessage) via React ContextWebviewRegistry — maps webview names → React components (in _integration/WebviewRegistry)Configuration from the extension host is read via useConfiguration<T>().
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/ # Helpers
DocumentView (simpler, good reference pattern):
DocumentView
├── ProgressBar (conditional: isLoading)
├── ToolbarDocuments
└── MonacoEditor
CollectionView (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)
useState + props[state, setState] tupleexport 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 },
}));
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 only
Why: 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.
useEffect(() => {
const handler = debounce(() => editorRef.current?.layout(), 200);
window.addEventListener('resize', handler);
handleResize(); // initial layout
return () => window.removeEventListener('resize', handler);
}, []);
return () => {
editorRef.current?.dispose();
};
<MonacoAutoHeight
adaptiveHeight={{ enabled: true, maxLines: 10, minLines: 1, lineHeight: 19 }}
onExecuteRequest={() => onExecuteRequest()}
onMount={(editor, monaco) => handleEditorDidMount(editor, monaco)}
/>
Use @fluentui/react-components (v9), themed via DynamicThemeProvider:
| 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
.scss file, imported directlysharedStyles.scss, applied via @extend10px with flexbox row-gap/column-gap.documentView {
display: flex;
flex-direction: column;
height: 100vh;
row-gap: 10px;
}
| 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). |
Object-based switch:
{{
'Table View': <DataViewPanelTable {...props} />,
'Tree View': <DataViewPanelTree {...props} />,
'JSON View': <DataViewPanelJSON {...props} />,
}[currentContext.currentView]}
const [isLoading, setIsLoading] = useState(false);
setIsLoading(true);
try {
await op();
} finally {
setIsLoading(false);
}
// In render:
{
isLoading && <ProgressBar thickness="large" shape="square" className="progressBar" />;
}
useEffect returneditor.layout() after resize → blank Monaco panelsany → use proper types or unknown with type guardsl10n.t() on user-facing stringsTake microsoft/react-webview-architecture from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.