In modern React (v18 & v19), the most robust and idiomatic pattern for multi-window docking is using
React Portals (createPortal)
. This preserves internal component state, React Context, and event bubbling without unmounting or remounting components when panels are dragged between stacks or popped out into auxiliary windows.
1. Installation
Install Strelit UI Kit from npm:
npm install strelit-ui-kit
2. The React Portal Docking Pattern
Instead of letting the layout engine imperatively manage DOM lifecycle, we register a generic component factory that hands container DOM elements over to React. React then projects your virtual DOM components into those containers seamlessly:
import React, { useState, useRef, useLayoutEffect } from 'react';
import { createPortal } from 'react-dom';
import { StrelitLayout, LayoutConfig, ComponentContainer } from 'strelit-ui-kit';
import 'strelit-ui-kit/dist/css/strelit-base.css';
import 'strelit-ui-kit/dist/css/themes/strelit-dark-theme.css';
interface PortalMount {
id: string;
element: HTMLElement;
component: React.ReactNode;
}
export function DockingWorkspace() {
const containerRef = useRef<HTMLDivElement>(null);
const layoutRef = useRef<StrelitLayout | null>(null);
const [portals, setPortals] = useState<PortalMount[]>([]);
useLayoutEffect(() => {
if (!containerRef.current) return;
const config: LayoutConfig = {
root: {
type: 'row',
content: [
{
type: 'component',
componentType: 'portalHost',
title: 'Telemetry Inspector',
componentState: { key: 'telemetry' }
},
{
type: 'component',
componentType: 'portalHost',
title: 'Event Log Stream',
componentState: { key: 'logs' }
}
]
}
};
// 1. Initialize StrelitLayout on the mounting DOM element
const layout = new StrelitLayout(containerRef.current);
// 2. Register generic host component that hands container element to React
layout.registerComponentFactoryFunction('portalHost', (container: ComponentContainer, itemConfig) => {
const el = container.element;
const state = (itemConfig.componentState as { key: string }) || { key: 'unknown' };
const key = state.key;
const component = key === 'telemetry'
? <TelemetryViewer />
: <LogStreamViewer />;
setPortals(prev => [...prev, { id: key, element: el, component }]);
container.on('destroy', () => {
setPortals(prev => prev.filter(p => p.id !== key));
});
});
// 3. Load layout configuration
layout.loadLayout(config);
layoutRef.current = layout;
const handleResize = () => layout.updateRootSize();
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
layout.destroy();
};
}, []);
return (
<div style={{ width: '100vw', height: '100vh', position: 'relative' }}>
<div ref={containerRef} style={{ width: '100%', height: '100%' }} />
{portals.map(p => createPortal(p.component, p.element, p.id))}
</div>
);
}
3. Advantages of the React 19 Portal Architecture
Preserved State: Form inputs, scroll positions, and local component hooks (
useState,useReducer) are never lost during panel drag, drop, or re-ordering.Context Propagation: All React Context providers (Zustand, Redux, TanStack Query, ThemeContext) remain intact across docked panes.
Automatic Cleanup: When a user closes a tab, Strelit triggers
container.on(‘destroy’), unmounting the portal cleanly and avoiding memory leaks.
⚡ Looking for multi-monitor Popout Windows in React?
Check out our dedicated Working with Popouts Tutorial to learn how Strelit coordinates state across separate browser windows.