| name | dockview |
| description | dockview-react panel layout library. Use when building multi-panel IDE-style layouts, implementing dockable/floatable panels, persisting layout state, or debugging dockview. Triggers on DockviewReact, IDockviewPanelProps, DockviewApi, DockviewReadyEvent, addPanel, fromJSON, toJSON, onReady, components prop. |
Dockview
dockview (package: dockview or dockview-react) provides a VS Code-style dockable panel shell for React apps. The DockviewReact component is the root; panels are registered as React components keyed by string name.
Critical: components prop identity
The most important thing to know about dockview: the components prop is compared by object reference on every render. If the reference changes, dockview treats all panels as new component types and unmounts + remounts every panel — causing a visible blank flash.
WRONG — new object and new React.FC identities on every render:
<DockviewReact
components={{ 'panel.id': wrapForDockview(MyPanel) }}
onReady={onReady}
/>
CORRECT — stable reference with useMemo:
const COMPONENTS = {
'panel.id': wrapForDockview(MyPanel)
};
const dockviewComponents = useMemo(
() => mergeComponents(COMPONENTS, panelComponentOverrides),
[panelComponentOverrides]
);
<DockviewReact components={dockviewComponents} onReady={onReady} />
The panelComponentOverrides object passed from a parent must also be stable — wrap it in useMemo in the parent:
<DockShell panelComponents={{ 'panel.id': MyPanel }} />
const overrides = useMemo(() => ({ 'panel.id': MyPanel }), [MyPanel]);
<DockShell panelComponents={overrides} />
Setup
import { DockviewReact } from 'dockview-react';
import type {
DockviewApi,
DockviewReadyEvent,
IDockviewPanelProps,
AddPanelOptions,
} from 'dockview-react';
import 'dockview-react/dist/styles/dockview.css';
function wrap<P extends object>(C: React.FC<P>): React.FC<IDockviewPanelProps> {
return function Wrapped() { return <C {...({} as P)} />; };
}
const COMPONENTS = {
'sidebar': wrap(Sidebar),
'editor': wrap(Editor),
'terminal': wrap(Terminal),
};
function Shell() {
const apiRef = useRef<DockviewApi | null>(null);
const onReady = useCallback((event: DockviewReadyEvent) => {
apiRef.current = event.api;
event.api.addPanel({ id: 'sidebar', component: 'sidebar' });
event.api.addPanel({ id: 'editor', component: 'editor', position: { referencePanel: 'sidebar', direction: 'right' } });
event.api.addPanel({ id: 'terminal', component: 'terminal', position: { referencePanel: 'editor', direction: 'below' } });
}, []);
return (
<DockviewReact
className="h-full"
components={COMPONENTS} // MUST be stable — see above
onReady={onReady}
/>
);
}
Layout persistence (toJSON / fromJSON)
Dockview serializes its own layout as a JSON blob. Save it on every change and restore on mount:
const onReady = useCallback((event: DockviewReadyEvent) => {
apiRef.current = event.api;
if (savedLayout?.dockview) {
try {
event.api.fromJSON(savedLayout.dockview);
} catch {
addDefaultPanels(event.api);
}
} else {
addDefaultPanels(event.api);
}
event.api.onDidLayoutChange(() => {
onSave(event.api.toJSON());
});
}, [savedLayout]);
Layout migration: dockview's native JSON is coupled to panel IDs and its own internal coordinate space. If panel IDs change across app versions, fromJSON may throw. Store a shape discriminator alongside the blob and handle both factory (addPanel) and native (fromJSON) paths.
Accessing the API imperatively
apiRef.current?.clear();
addDefaultPanels(apiRef.current!);
apiRef.current?.getPanel('editor')?.focus();
apiRef.current?.getPanel('terminal')?.api.close();
Testing (jsdom / vitest)
Dockview's layout engine uses ResizeObserver and getBoundingClientRect — both unavailable in jsdom. Mock the module:
vi.mock('dockview-react', () => ({
DockviewReact: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
}));
Tests should assert against accessible roles or data-testid on panel content; the DockviewReact stub is just a div.
Reference