Skip to content

Controlling Nested Layouts ​

For complex IDE-like layouts with nested splits, you can control specific nested panes using multiple refs or controllers. Each Split component maintains its own state, so you need a separate ref or controller for each level you want to control.


Using Ref API for Nested Layouts ​

The simplest approach for nested control - use a separate ref for each Split you want to control.

tsx
import { useRef } from 'react';
import { Split, type SplitRef } from '@a-multilayout-splitter/core';

function NestedRefLayout() {
  // Ref for outer horizontal split
  const outerRef = useRef<SplitRef>(null);

  // Ref for inner vertical split
  const innerRef = useRef<SplitRef>(null);

  return (
    <div>
      <header>
        {/* Control outer split */}
        <button onClick={() => outerRef.current?.togglePane(0)}>
          Toggle Sidebar
        </button>
        <button onClick={() => outerRef.current?.togglePane(2)}>
          Toggle Right Panel
        </button>

        {/* Control inner split */}
        <button onClick={() => innerRef.current?.togglePane(1)}>
          Toggle Terminal
        </button>
      </header>

      {/* Outer horizontal split */}
      <Split ref={outerRef} mode="horizontal" initialSizes={['20%', '50%', '30%']}>
        <div>Sidebar</div>

        {/* Nested vertical split */}
        <Split ref={innerRef} mode="vertical" initialSizes={['70%', '30%']}>
          <div>Editor</div>
          <div>Terminal</div>
        </Split>

        <div>Right Panel</div>
      </Split>
    </div>
  );
}

Why use Ref API for nested layouts? ​

  • Simple and direct: Just add a ref to each Split you need to control.
  • No extra state: The Split component manages its own state internally.
  • Performance: Ref methods don't cause parent re-renders.

Using Hook API for Nested Layouts ​

When you need to read and react to pane state (e.g., show different UI based on collapsed state), use useSplitController for each nested Split.

tsx
import { useSplitController, Split } from '@a-multilayout-splitter/core';

function NestedHookLayout() {
  // Controller for outer horizontal split
  const outerController = useSplitController({
    mode: 'horizontal',
    initialPanes: [
      { id: 'sidebar', size: '20%', collapsed: false, minSize: 10, maxSize: 40, content: null },
      { id: 'center', size: '50%', collapsed: false, minSize: 30, maxSize: 70, content: null },
      { id: 'right', size: '30%', collapsed: false, minSize: 15, maxSize: 50, content: null },
    ],
  });

  // Controller for inner vertical split
  const innerController = useSplitController({
    mode: 'vertical',
    initialPanes: [
      { id: 'editor', size: '70%', collapsed: false, minSize: 30, maxSize: 85, content: null },
      { id: 'terminal', size: '30%', collapsed: false, minSize: 15, maxSize: 50, content: null },
    ],
  });

  // Map collapsed states to pass to Split components
  const outerCollapsed = outerController.panes.map(p => p.collapsed);
  const innerCollapsed = innerController.panes.map(p => p.collapsed);

  return (
    <div>
      <header>
        {/* Outer controls with reactive labels */}
        <button onClick={() => outerController.togglePane(0)}>
          {outerController.panes[0]?.collapsed ? 'Show' : 'Hide'} Sidebar
        </button>
        <button onClick={() => outerController.togglePane(2)}>
          {outerController.panes[2]?.collapsed ? 'Show' : 'Hide'} Right Panel
        </button>

        {/* Inner controls with reactive labels */}
        <button onClick={() => innerController.togglePane(1)}>
          {innerController.panes[1]?.collapsed ? 'Show' : 'Hide'} Terminal
        </button>

        {/* Display current state */}
        <span>
          Sidebar: {outerController.panes[0]?.collapsed ? 'closed' : 'open'} |
          Terminal: {innerController.panes[1]?.collapsed ? 'closed' : 'open'}
        </span>
      </header>

      {/* Outer split */}
      <Split
        mode="horizontal"
        initialSizes={['20%', '50%', '30%']}
        collapsed={outerCollapsed}
        minSizes={[10, 30, 15]}
      >
        <div>Sidebar</div>

        {/* Inner split controlled by hook */}
        <Split
          mode="vertical"
          initialSizes={['70%', '30%']}
          collapsed={innerCollapsed}
          minSizes={[30, 15]}
        >
          <div>Editor</div>
          <div>Terminal</div>
        </Split>

        <div>Right Panel</div>
      </Split>
    </div>
  );
}

Why use Hook API for nested layouts? ​

  • Reactive UI: Button labels and status indicators update automatically.
  • State access: Read pane state directly from controller.panes.
  • External sync: Integrate with Redux, Zustand, or localStorage via onPaneChange.

Mixed Approach (Ref + Hook) ​

Use Ref API for simple toggle actions and Hook API only where you need reactive state. This gives you the best of both worlds.

tsx
import { useRef } from 'react';
import { Split, type SplitRef, useSplitController } from '@a-multilayout-splitter/core';

function MixedNestedLayout() {
  // Ref for outer split (simple toggle, no state needed)
  const outerRef = useRef<SplitRef>(null);

  // Ref for center split (simple toggle)
  const centerRef = useRef<SplitRef>(null);

  // Hook for right panel (need reactive state for UI)
  const rightPanelController = useSplitController({
    mode: 'vertical',
    initialPanes: [
      { id: 'preview', size: '60%', collapsed: false, minSize: 20, maxSize: 80, content: null },
      { id: 'console', size: '40%', collapsed: false, minSize: 15, maxSize: 60, content: null },
    ],
  });

  // Map collapsed state for the right panel
  const rightPanelCollapsed = rightPanelController.panes.map(p => p.collapsed);

  return (
    <div>
      <header>
        {/* Outer controls via Ref (simple) */}
        <button onClick={() => outerRef.current?.togglePane(0)}>
          Toggle Sidebar
        </button>

        {/* Center controls via Ref (simple) */}
        <button onClick={() => centerRef.current?.togglePane(1)}>
          Toggle Terminal
        </button>

        {/* Right panel controls via Hook (reactive) */}
        <button onClick={() => rightPanelController.togglePane(1)}>
          {rightPanelController.panes[1]?.collapsed ? 'Show' : 'Hide'} Console
        </button>
      </header>

      <Split
        ref={outerRef}
        mode="horizontal"
        initialSizes={['20%', '50%', '30%']}
        minSizes={[10, 30, 15]}
      >
        <div>Sidebar</div>

        {/* Center - Ref API */}
        <Split ref={centerRef} mode="vertical" initialSizes={['70%', '30%']} minSizes={[30, 15]}>
          <div>Editor</div>
          <div>Terminal</div>
        </Split>

        {/* Right panel - Hook API for reactive state */}
        <Split
          mode="vertical"
          initialSizes={['60%', '40%']}
          collapsed={rightPanelCollapsed}
          minSizes={[20, 15]}
        >
          <div>Preview</div>
          <div>Console ({rightPanelController.panes[1]?.collapsed ? 'hidden' : 'visible'})</div>
        </Split>
      </Split>
    </div>
  );
}

When to use Mixed Approach? ​

  • Ref for simple toggle buttons where you don't need to know the current state.
  • Hook for UI that needs to react to pane state (dynamic labels, status indicators, conditional rendering).

Key Points ​

PointDescription
One ref/controller per SplitEach Split needs its own ref or controller
Pane indices are localtogglePane(1) refers to index 1 of that specific Split
Parent collapse hides childrenCollapsing an outer pane hides all nested Splits inside
State is independentEach Split maintains its own state separately

Released under the MIT License.