flexlayout-react について
flexlayout-reactは、カスタマイズ可能なドッキングUIを構築するReactパッケージです。タブ、ドラッグ可能なパネル、サイズ変更できるペイン、保存可能なレイアウトを備え、複雑なアプリ画面に対応します。
主な機能
- 01ドラッグ&ドロップによるパネル配置
- 02ウィンドウ操作付きタブパネル
- 03サイズ変更可能なペインと境界領域
- 04レイアウトの保存と復元
- 05タブとレイアウトのプログラム制御
- 06カスタムスタイルとTypeScript対応
活用シーン
- 01IDE風ワークスペースの構築
- 02設定可能なダッシュボードの作成
- 03管理画面のペイン整理
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ bobmatnyc/ claude-mpm-skills --skill flexlayout-reactインストール後、Claude Code で/flexlayout-reactを入力して読み込まれたか確認します。
同梱ファイルmetadata.json
詳細
FlexLayout-React - プロフェッショナルなドッキングレイアウト
概要
FlexLayout-React は、ドラッグ&ドロップ、タブ、スプリッター、複雑なウィンドウ管理を備えた、IDE 品質のドッキングレイアウトを提供します。ダッシュボード、IDE、管理パネルなど、柔軟でユーザーがカスタマイズできるレイアウトを必要とするあらゆるインターフェースに最適です。
主な機能:
- ドラッグ&ドロップによるパネルの再配置
- 閉じる、最大化、最小化が可能なタブインターフェース
- サイズ変更可能なペイン用のスプリッター
- 境界へのドッキング領域
- レイアウトの永続化(保存/復元)
- プログラムによるレイアウト制御
- TypeScript のサポート
インストール:
npm install flexlayout-react
基本的なセットアップ
1. レイアウトモデルを定義する
import { Model, IJsonModel } from 'flexlayout-react';
const initialLayout: IJsonModel = {
global: {
tabEnableClose: true,
tabEnableRename: false,
},
borders: [],
layout: {
type: 'row',
weight: 100,
children: [
{
type: 'tabset',
weight: 50,
children: [
{
type: 'tab',
name: 'Explorer',
component: 'explorer',
}
]
},
{
type: 'tabset',
weight: 50,
children: [
{
type: 'tab',
name: 'Editor',
component: 'editor',
}
]
}
]
}
};
// Create model
const model = Model.fromJson(initialLayout);
2. レイアウトコンポーネントを作成する
import React, { useRef } from 'react';
import { Layout, Model, TabNode, IJsonTabNode } from 'flexlayout-react';
import 'flexlayout-react/style/dark.css'; // or light.css
interface ComponentRegistry {
explorer: React.ComponentType;
editor: React.ComponentType;
terminal: React.ComponentType;
}
function App() {
const modelRef = useRef(Model.fromJson(initialLayout));
const factory = (node: TabNode) => {
const component = node.getComponent();
switch (component) {
case 'explorer':
return <ExplorerPanel />;
case 'editor':
return <EditorPanel />;
case 'terminal':
return <TerminalPanel />;
default:
return <div>Unknown component: {component}</div>;
}
};
return (
<div style={{ width: '100vw', height: '100vh' }}>
<Layout
model={modelRef.current}
factory={factory}
/>
</div>
);
}
3. コンポーネントの実装
function ExplorerPanel() {
return (
<div className="panel-explorer">
<h3>File Explorer</h3>
<ul>
<li>src/</li>
<li>public/</li>
<li>package.json</li>
</ul>
</div>
);
}
function EditorPanel() {
return (
<div className="panel-editor">
<textarea
style={{ width: '100%', height: '100%' }}
placeholder="Start typing..."
/>
</div>
);
}
高度なレイアウト設定
複雑なマルチペインレイアウト
const complexLayout: IJsonModel = {
global: {
tabEnableClose: true,
tabEnableRename: false,
tabEnableDrag: true,
tabEnableFloat: true,
borderSize: 200,
},
borders: [
{
type: 'border',
location: 'left',
size: 250,
children: [
{
type: 'tab',
name: 'Explorer',
component: 'explorer',
}
]
},
{
type: 'border',
location: 'bottom',
size: 200,
children: [
{
type: 'tab',
name: 'Terminal',
component: 'terminal',
},
{
type: 'tab',
name: 'Output',
component: 'output',
}
]
}
],
layout: {
type: 'row',
weight: 100,
children: [
{
type: 'tabset',
weight: 70,
children: [
{
type: 'tab',
name: 'Editor 1',
component: 'editor',
},
{
type: 'tab',
name: 'Editor 2',
component: 'editor',
}
]
},
{
type: 'tabset',
weight: 30,
children: [
{
type: 'tab',
name: 'Properties',
component: 'properties',
},
{
type: 'tab',
name: 'Outline',
component: 'outline',
}
]
}
]
}
};
ネストされた行と列
const nestedLayout: IJsonModel = {
global: {},
borders: [],
layout: {
type: 'row',
children: [
{
type: 'col',
weight: 50,
children: [
{
type: 'tabset',
weight: 70,
children: [
{ type: 'tab', name: 'Top Left', component: 'panel-a' }
]
},
{
type: 'tabset',
weight: 30,
children: [
{ type: 'tab', name: 'Bottom Left', component: 'panel-b' }
]
}
]
},
{
type: 'col',
weight: 50,
children: [
{
type: 'tabset',
weight: 30,
children: [
{ type: 'tab', name: 'Top Right', component: 'panel-c' }
]
},
{
type: 'tabset',
weight: 70,
children: [
{ type: 'tab', name: 'Bottom Right', component: 'panel-d' }
]
}
]
}
]
}
};
レイアウトの永続化
レイアウトの保存と復元
import { useState, useEffect } from 'react';
import { Model, Actions } from 'flexlayout-react';
function LayoutManager() {
const [model, setModel] = useState(() => {
// Load from localStorage
const saved = localStorage.getItem('layout');
return saved
? Model.fromJson(JSON.parse(saved))
: Model.fromJson(defaultLayout);
});
// Save on model change
const onModelChange = (newModel: Model) => {
const json = newModel.toJson();
localStorage.setItem('layout', JSON.stringify(json));
};
return (
<Layout
model={model}
factory={factory}
onModelChange={onModelChange}
/>
);
}
デフォルトレイアウトへのリセット
function LayoutControls({ model }: { model: Model }) {
const resetLayout = () => {
const newModel = Model.fromJson(defaultLayout);
// Need to replace model reference
window.location.reload(); // Simple approach
};
const saveLayout = () => {
const json = model.toJson();
const blob = new Blob([JSON.stringify(json, null, 2)], {
type: 'application/json'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'layout.json';
a.click();
};
return (
<div className="layout-controls">
<button onClick={resetLayout}>Reset Layout</button>
<button onClick={saveLayout}>Export Layout</button>
</div>
);
}
動的なタブ管理
プログラムによるタブの追加
import { Actions, DockLocation } from 'flexlayout-react';
function addNewTab(model: Model, tabsetId: string) {
model.doAction(Actions.addNode(
{
type: 'tab',
name: `New Tab ${Date.now()}`,
component: 'editor',
},
tabsetId,
DockLocation.CENTER,
-1
));
}
// Add to specific tabset
const addToExplorer = () => {
addNewTab(model, 'explorer-tabset-id');
};
// Add to active tabset
const addToActive = () => {
const activeTabset = model.getActiveTabset();
if (activeTabset) {
addNewTab(model, activeTabset.getId());
}
};
タブを閉じる
function closeTab(model: Model, tabId: string) {
model.doAction(Actions.deleteTab(tabId));
}
function closeAllTabs(model: Model) {
const tabsets = model.getRoot().getChildren();
tabsets.forEach(tabset => {
if (tabset.getType() === 'tabset') {
const tabs = tabset.getChildren();
tabs.forEach(tab => {
if (tab.getType() === 'tab') {
model.doAction(Actions.deleteTab(tab.getId()));
}
});
}
});
}
タブのコンテキストと Props
コンポーネントへのデータの受け渡し
interface EditorTabProps {
node: TabNode;
}
function EditorTab({ node }: EditorTabProps) {
const filepath = node.getConfig()?.filepath as string;
const readonly = node.getConfig()?.readonly as boolean;
return (
<div>
<p>Editing: {filepath}</p>
<textarea readOnly={readonly} />
</div>
);
}
// Factory with data passing
const factory = (node: TabNode) => {
const component = node.getComponent();
switch (component) {
case 'editor':
return <EditorTab node={node} />;
default:
return <div>Unknown</div>;
}
};
// Create tab with config
const newTab: IJsonTabNode = {
type: 'tab',
name: 'my-file.ts',
component: 'editor',
config: {
filepath: '/src/my-file.ts',
readonly: false,
}
};
タブの状態へのアクセス
function SmartPanel({ node }: { node: TabNode }) {
const name = node.getName();
const isActive = node.isSelected();
const isVisible = node.isVisible();
return (
<div className={isActive ? 'active' : 'inactive'}>
<h3>{name}</h3>
{isVisible && <p>This tab is visible</p>}
</div>
);
}
スタイリングとテーマ設定
カスタム CSS
/* Override FlexLayout styles */
.flexlayout__layout {
background: #1e1e1e;
}
.flexlayout__tab {
background: #2d2d2d;
color: #cccccc;
}
.flexlayout__tab:hover {
background: #3e3e3e;
}
.flexlayout__tab_button--selected {
background: #1e1e1e;
border-bottom: 2px solid #007acc;
}
.flexlayout__splitter {
background: #2d2d2d;
}
.flexlayout__splitter:hover {
background: #007acc;
}
ダーク/ライトテーマの切り替え
import 'flexlayout-react/style/dark.css';
// or
import 'flexlayout-react/style/light.css';
function ThemeToggle() {
const [theme, setTheme] = useState<'dark' | 'light'>('dark');
useEffect(() => {
// Dynamically load theme
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = `flexlayout-react/style/${theme}.css`;
document.head.appendChild(link);
return () => {
document.head.removeChild(link);
};
}, [theme]);
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Toggle Theme
</button>
);
}
Tauri との統合
レイアウトを Tauri バックエンドに永続化する
import { invoke } from '@tauri-apps/api/core';
async function saveLayoutToTauri(model: Model) {
const json = model.toJson();
await invoke('save_layout', {
layout: JSON.stringify(json)
});
}
async function loadLayoutFromTauri(): Promise<Model> {
const layout = await invoke<string>('load_layout');
return Model.fromJson(JSON.parse(layout));
}
// Tauri command (Rust)
// #[tauri::command]
// async fn save_layout(layout: String) -> Result<(), String> {
// let app_dir = app.path_resolver().app_data_dir()?;
// let layout_file = app_dir.join("layout.json");
// tokio::fs::write(layout_file, layout).await?;
// Ok(())
// }
ウィンドウごとのレイアウト
import { invoke } from '@tauri-apps/api/core';
import { getCurrent } from '@tauri-apps/api/window';
function WindowLayout() {
const [model, setModel] = useState<Model | null>(null);
useEffect(() => {
const currentWindow = getCurrent();
const windowLabel = currentWindow.label;
// Load layout for this specific window
invoke<string>('load_window_layout', { windowLabel })
.then(layout => {
setModel(Model.fromJson(JSON.parse(layout)));
})
.catch(() => {
setModel(Model.fromJson(defaultLayout));
});
}, []);
const onModelChange = (newModel: Model) => {
const currentWindow = getCurrent();
const json = newModel.toJson();
invoke('save_window_layout', {
windowLabel: currentWindow.label,
layout: JSON.stringify(json)
});
};
if (!model) return <div>Loading...</div>;
return (
<Layout
model={model}
factory={factory}
onModelChange={onModelChange}
/>
);
}
高度なパターン
カスタムタブヘッダー
import { Layout, Model, TabNode, ITabRenderValues } from 'flexlayout-react';
function App() {
const onRenderTab = (
node: TabNode,
renderValues: ITabRenderValues
) => {
const modified = node.getConfig()?.modified as boolean;
renderValues.content = (
<div className="custom-tab-header">
<span>{node.getName()}</span>
{modified && <span className="modified-indicator">●</span>}
</div>
);
};
return (
<Layout
model={model}
factory={factory}
onRenderTab={onRenderTab}
/>
);
}
タブのアクション(カスタムボタン)
const onRenderTab = (node: TabNode, renderValues: ITabRenderValues) => {
renderValues.buttons.push(
<button
key="save"
onClick={() => saveTabContent(node)}
title="Save"
>
💾
</button>
);
renderValues.buttons.push(
<button
key="duplicate"
onClick={() => duplicateTab(node)}
title="Duplicate"
>
📋
</button>
);
};
ベストプラクティス
- レイアウトを永続化する - ユーザー体験のために localStorage またはバックエンドに保存します
- 一意なコンポーネント名を使用する - ファクトリー関数での衝突を避けます
- コンポーネントの欠落に対応する - ファクトリーにデフォルトケースを用意します
- ファクトリー関数をメモ化する - 不要な再レンダリングを防ぎます
- タブデータには config を使用する - タブ固有の Props を config に保存します
- リセット手段を用意する - ユーザーがデフォルトレイアウトに戻せるようにします
- レイアウトの変更をテストする - 永続化が正しく動作することを確認します
- エッジケースに対応する - 空のタブセット、削除されたコンポーネントなどを処理します
- 境界を適切に使う - ツールには左/右/上/下を、コンテンツにはメイン領域を使用します
- 大規模なレイアウトを最適化する - 可能であればコンポーネントを遅延読み込みします
よくある落とし穴
❌ モデルをメモ化していない:
// WRONG - creates new model on every render
function App() {
const model = Model.fromJson(layout); // Bad!
return <Layout model={model} />;
}
// CORRECT
function App() {
const modelRef = useRef(Model.fromJson(layout));
return <Layout model={modelRef.current} />;
}
❌ CSS のインポートを忘れている:
// WRONG - layout won't display correctly
import { Layout } from 'flexlayout-react';
// Missing: import 'flexlayout-react/style/dark.css';
❌ onModelChange に対応していない:
// WRONG - layout changes not persisted
<Layout model={model} factory={factory} />
// CORRECT
<Layout
model={model}
factory={factory}
onModelChange={saveLayout}
/>
リソース
- ドキュメント: https://github.com/caplin/FlexLayout
- サンプル: https://rawgit.com/caplin/FlexLayout/demos/demos/index.html
- TypeScript の型定義: パッケージに含まれています
まとめ
- FlexLayout は IDE 品質のドッキングレイアウトを提供します
- モデル駆動型 - レイアウトを JSON として定義し、プログラムで制御します
- 永続化 - ユーザーレイアウトを簡単に保存/復元できます
- カスタマイズ可能 - カスタムタブ、境界、テーマに対応します
- React との親和性 - Hooks、TypeScript をサポートします
- 最適な用途 - IDE、ダッシュボード、管理パネル、複雑な UI
- Tauri との統合 - バックエンドへの永続化、ウィンドウごとのレイアウトに対応します
関連スキル
FlexLayout-React の使用時には、次のスキルでワークフローを強化できます。
- react-state-machine: 複雑な UI ロジック、マルチステップフォーム、非同期フローに対応する XState v5 のステートマシンとアクターモデル
- react-hooks-composition: データ取得とパフォーマンスのための、高度な React Hooks 合成パターン
- tanstack-query: キャッシュと再取得機能を備えた、React アプリ向けのサーバー状態管理
- zustand: Redux に代わる軽量なクライアント状態管理
- nextjs: SSR、ルーティング、フルスタック機能を備えた React フレームワーク
- test-driven-development: React コンポーネントと Hooks の TDD パターン
[これらのスキルがバンドルにデプロイされている場合は、完全なドキュメントを利用できます]
コメント
Markdown 対応