Skip to content

Advanced Topics

Pre-loading a layout with a specific initial tree

Use WorkspaceClient.initialState to define a precise default layout rather than relying on drag-and-drop configuration by the user. The value is the same JSON produced by saveLayout().

ts
const DEFAULT_LAYOUT = JSON.stringify({
  gridRoot: {
    type: 'branch',
    orientation: 'horizontal',
    sizes: [0.7, 0.3],
    children: [
      {
        type: 'leaf',
        id: 'main-area',
        panels: ['map-1'],
        activePanelId: 'map-1',
      },
      {
        type: 'branch',
        orientation: 'vertical',
        sizes: [0.5, 0.5],
        children: [
          { type: 'leaf', id: 'top-right',    panels: ['props-1'],   activePanelId: 'props-1' },
          { type: 'leaf', id: 'bottom-right', panels: ['console-1'], activePanelId: 'console-1' },
        ],
      },
    ],
  },
  floating: [],
  minimized: [],
  panels: {
    'map-1':     { id: 'map-1',     title: 'Map',        component: 'map',     state: 'docked' },
    'props-1':   { id: 'props-1',   title: 'Properties', component: 'props',   state: 'docked' },
    'console-1': { id: 'console-1', title: 'Console',    component: 'console', state: 'docked' },
  },
});

const client = new WorkspaceClient({
  panels: {
    map:     { component: MapPanel },
    props:   { component: PropertiesPanel },
    console: { component: ConsolePanel },
  },
  initialState: localStorage.getItem('workspace-layout') ?? DEFAULT_LAYOUT,
});

The initialState is read once at construction time — it is not reactive.

Zero-unmount DOM preservation

Heavy widgets (WebGL contexts, Leaflet maps, CodeMirror instances) maintain their DOM nodes when a panel is hidden, minimized, or covered by another tab. The library moves the DOM subtree into a hidden container (#preserved-dom-container) rather than unmounting it.

This is automatic — you do not need to configure anything. The implication is that your panel components should be written to tolerate visibility changes without relying on mount/unmount cycles.

If you need to react to visibility, use the lifecycle hooks:

ts
import { usePanelContext } from 'react-dockable-desktop';

function MyPanel({ panelId }: { panelId: string }) {
  const { onRestore, onMinimize } = usePanelContext(panelId);

  useEffect(() => {
    const unsubRestore  = onRestore(() => { /* panel became visible */ });
    const unsubMinimize = onMinimize(() => { /* panel was hidden */ });
    return () => { unsubRestore(); unsubMinimize(); };
  }, []);
}

Custom header actions

Inject React nodes into a panel's tab header:

ts
const client = new WorkspaceClient({
  panels: {
    chart: {
      component: ChartPanel,
      defaultOptions: {
        renderHeaderActions: (panelId) => (
          <button onClick={() => exportChart(panelId)}>Export</button>
        ),
      },
    },
  },
});

RTL support

See the dedicated RTL Support → guide for the full wiring pattern, what flips automatically, macOS skin behaviour, and the isElementRtl utility.

In short: pass dir="rtl" to DockableDesktopProvider and set document.documentElement.dir = 'rtl' so portals (ContextMenu, toolbar flyout, Toast) that render into document.body also pick up the RTL direction via CSS inheritance.

Multiple providers on one page

Each WorkspaceClient instance has its own scoped PanelRegistryClass. Multiple providers can coexist on the same page without panel key conflicts:

tsx
const wsA = new WorkspaceClient({ panels: { map: { component: MapA } } });
const wsB = new WorkspaceClient({ panels: { map: { component: MapB } } });

<div>
  <WindowManagerProvider client={wsA}><WindowManager /></WindowManagerProvider>
  <WindowManagerProvider client={wsB}><WindowManager /></WindowManagerProvider>
</div>

DockableDesktopProvider, state selectors, lifecycle callbacks, usePanelId()

These v3 features are documented in the dedicated guides:

i18n / custom messages

Pass a formatMessage function to translate all built-in strings:

ts
import { useIntl } from 'react-intl';

function App() {
  const intl = useIntl();
  const client = useMemo(() => new WorkspaceClient({
    panels: { ... },
    formatMessage: (msg) => intl.formatMessage({ id: msg.id, defaultMessage: msg.defaultMessage }, msg.values),
  }), [intl]);

  return <WindowManagerProvider client={client}>...</WindowManagerProvider>;
}

Released under the MIT License.