はじめに
モダンなフロントエンド開発において、UIコンポーネントの開発と管理は重要な課題の一つです。今回は、その課題を解決するツール「Storybook」について、その特徴と実践的な使い方を紹介します。
Storybookとは
Storybookは、UIコンポーネントの開発環境として機能するオープンソースツールです。
以下のような特徴があります。
- コンポーネントを独立した環境で開発・テストできる
- インタラクティブなドキュメントとしても機能する
- 様々なフレームワーク(React、Vue、Angular等)に対応
- 豊富なアドオンによる拡張性
セットアップ方法
1. プロジェクトへの導入
# 既存プロジェクトに Storybook を追加
npx storybook init
2. 基本的な構成
Storybookをインストールすると、以下のようなファイル構造が生成されます。
.storybook/
├── main.js # Storybook の設定ファイル
└── preview.js # Story のグローバル設定
src/
└── stories/ # Story ファイル用ディレクトリ
Storyファイルの作成単位
Storyファイルの作成対象単位を表にまとめてみましょう。
作成対象 | 説明 |
---|---|
コンポーネント (Component) |
単一のUIコンポーネント (Button, Input, Cardなど) |
複合コンポーネント (Composite) |
複数のコンポーネントを組み合わせた機能 (Form, Dialog, Tableなど) |
レイアウト (Layout) |
ページ構造を定義するコンポーネント (Header, Sidebar, Gridなど) |
ページ (Page) |
完全なページ単位のコンポーネント (Login, Dashboard, Settingsなど) |
テンプレート (Template) |
再利用可能なページの雛形 (ArticleTemplate, ProductTemplate など) |
フック (Hook) |
カスタムフックの使用例 (useForm, useAuth などのカスタムフック) |
ユーティリティ (Utility) |
共通の機能やヘルパー (DataFormatter, ThemeProvider など) |
パターン (Pattern) |
特定の UI パターン (LoadingState, ErrorBoundary など) |
これらの作成対象は、プロジェクトの規模や要件に応じて適切に選択します。
必ずしもすべての対象にStoryを作成する必要はありません。
Storyの書き方
Storyはコンポーネントの使用例を示すためのファイルです。以下は基本的な作成例です。
1. コンポーネント(Component)
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'ボタン',
},
};
2. 複合コンポーネント(Composite)
// LoginForm.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { LoginForm } from './LoginForm';
const meta: Meta<typeof LoginForm> = {
title: 'Forms/LoginForm',
component: LoginForm,
parameters: {
layout: 'centered',
},
};
export default meta;
type Story = StoryObj<typeof LoginForm>;
export const Default: Story = {
args: {
onSubmit: (data) => console.log('Form submitted:', data),
},
};
export const WithError: Story = {
args: {
error: 'Invalid credentials',
onSubmit: (data) => console.log('Form submitted:', data),
},
};
3. レイアウト(Layout)
// Sidebar.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Sidebar } from './Sidebar';
const meta: Meta<typeof Sidebar> = {
title: 'Layout/Sidebar',
component: Sidebar,
parameters: {
layout: 'fullscreen',
},
};
export default meta;
type Story = StoryObj<typeof Sidebar>;
export const Default: Story = {
args: {
items: [
{ label: 'Dashboard', icon: 'home' },
{ label: 'Settings', icon: 'settings' },
],
},
};
4. ページ(Page)
// DashboardPage.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { DashboardPage } from './DashboardPage';
const meta: Meta<typeof DashboardPage> = {
title: 'Pages/Dashboard',
component: DashboardPage,
parameters: {
layout: 'fullscreen',
backgrounds: {
default: 'light',
},
},
};
export default meta;
type Story = StoryObj<typeof DashboardPage>;
export const Default: Story = {
args: {
username: 'John Doe',
stats: {
totalUsers: 1234,
activeUsers: 789,
},
},
};
5. テンプレート(Template)
// ArticleTemplate.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { ArticleTemplate } from './ArticleTemplate';
const meta: Meta<typeof ArticleTemplate> = {
title: 'Templates/Article',
component: ArticleTemplate,
parameters: {
layout: 'fullscreen',
},
};
export default meta;
type Story = StoryObj<typeof ArticleTemplate>;
export const Default: Story = {
args: {
title: '記事タイトル',
content: '記事の本文...',
author: {
name: '著者名',
avatar: '/avatar.jpg',
},
},
};
6. フック(Hook)
// useCounter.stories.tsx
import { useCounter } from './useCounter';
import { Meta, StoryObj } from '@storybook/react';
// フックを表示するためのラッパーコンポーネント
const CounterDemo = () => {
const { count, increment, decrement } = useCounter(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
};
const meta: Meta<typeof CounterDemo> = {
title: 'Hooks/useCounter',
component: CounterDemo,
};
export default meta;
type Story = StoryObj<typeof CounterDemo>;
export const Default: Story = {};
7. ユーティリティ(Utility)
// ThemeProvider.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { ThemeProvider } from './ThemeProvider';
import { Button } from '../components/Button';
const meta: Meta<typeof ThemeProvider> = {
title: 'Utilities/ThemeProvider',
component: ThemeProvider,
};
export default meta;
type Story = StoryObj<typeof ThemeProvider>;
export const Default: Story = {
render: () => (
<ThemeProvider theme="light">
<Button>Themed Button</Button>
</ThemeProvider>
),
};
8. パターン(Pattern)
// LoadingState.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { LoadingState } from './LoadingState';
const meta: Meta<typeof LoadingState> = {
title: 'Patterns/LoadingState',
component: LoadingState,
};
export default meta;
type Story = StoryObj<typeof LoadingState>;
export const Default: Story = {
args: {
isLoading: true,
children: <div>コンテンツ</div>,
},
};
export const WithCustomSpinner: Story = {
args: {
isLoading: true,
spinner: <div>カスタムローディング...</div>,
children: <div>コンテンツ</div>,
},
};
アドオンの活用
Storybookの機能は、アドオンによって拡張できます。特に有用なアドオンを紹介します。
アドオン名 | 説明 |
---|---|
Controls | コンポーネントのプロパティをGUI上で動的に変更可能 |
Accessibility | アクセシビリティチェックを自動実行 |
Viewport | 様々な画面サイズでのレイアウト確認 |
Actions | ユーザーイベントのモニタリング |
Docs | 自動ドキュメント生成 |
Interactions | インタラクティブなテストの作成と実行 |
CI/CDへの統合
Storybookは、CIプロセスに組み込むことができます。
以下に主要なCIプラットフォームでの設定例を示します。
GitHub Actions
# .github/workflows/storybook.yml
name: Storybook Tests
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- run: npm ci
- run: npm run build-storybook
- run: npm run test-storybook
GitLab CI
# .gitlab-ci.yml
storybook:
image: node:latest
stage: test
script:
- npm ci
- npm run build-storybook
- npm run test-storybook
artifacts:
paths:
- storybook-static/
まとめ
Storybookを導入することで、以下のメリットが得られます。
Storybookを使おう!
-
コンポーネントの開発効率の向上
-
品質管理の効率化
-
ドキュメント作成の自動化