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.