Storybook × React:現場で役立つベストプラクティス 10 選

Storybook を導入してみたものの、「基本的な使い方はわかったけど、現場でもっと活用するにはどうすれば?」と感じているエンジニアの方も多いのではないでしょうか。
シンプルなストーリーは作れるようになったけれど、複雑な状態管理や API 連携、パフォーマンス最適化など、実際のプロダクト開発で直面する課題にどう対応すれば良いのか。そんな疑問をお持ちの方に向けて、現場で本当に役立つ実践的なテクニックをお伝えします。
今回ご紹介する 10 のベストプラクティスは、実際のプロダクト開発現場で培われた知見を基に厳選しました。これらを活用することで、Storybook が単なる「コンポーネントカタログ」から「開発体験を劇的に向上させる強力なツール」へと変貌することでしょう。
ベストプラクティス 1:Advanced Controls でリアルタイムデバッグ
基本的な Controls アドオンは多くの方が使っていると思いますが、その高度な機能を活用することで、デバッグ効率を大幅に向上させることができます。
オブジェクト型のコントロール活用
typescript// 複雑なオブジェクトを直接操作可能に
export interface UserProfileProps {
user: {
id: string;
name: string;
email: string;
preferences: {
theme: 'light' | 'dark';
notifications: boolean;
privacy: 'public' | 'friends' | 'private';
};
};
onUpdate: (user: UserProfileProps['user']) => void;
}
export const UserProfile: React.FC<UserProfileProps> = ({
user,
onUpdate,
}) => {
return (
<div
className={`profile profile--${user.preferences.theme}`}
>
<h2>{user.name}</h2>
<p>{user.email}</p>
<div className='preferences'>
<label>
<input
type='checkbox'
checked={user.preferences.notifications}
onChange={(e) =>
onUpdate({
...user,
preferences: {
...user.preferences,
notifications: e.target.checked,
},
})
}
/>
通知を受け取る
</label>
</div>
</div>
);
};
Advanced Controls の設定
typescript// UserProfile.stories.ts
import type { Meta, StoryObj } from '@storybook/react';
import { UserProfile } from './UserProfile';
const meta: Meta<typeof UserProfile> = {
title: 'Advanced/UserProfile',
component: UserProfile,
argTypes: {
user: {
control: 'object',
description: 'ユーザー情報の複雑なオブジェクト',
},
'user.preferences.theme': {
control: 'select',
options: ['light', 'dark'],
},
'user.preferences.privacy': {
control: 'select',
options: ['public', 'friends', 'private'],
},
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Interactive: Story = {
args: {
user: {
id: '1',
name: '田中太郎',
email: 'tanaka@example.com',
preferences: {
theme: 'light',
notifications: true,
privacy: 'friends',
},
},
},
parameters: {
docs: {
description: {
story:
'リアルタイムでユーザーオブジェクトを編集し、即座に反映される様子を確認できます。',
},
},
},
};
カスタムコントロールの作成
更に高度な活用として、独自のコントロールを作成することも可能です。
typescript// 日付範囲ピッカーのカスタムコントロール
const dateRangeControl = {
control: {
type: 'object',
},
description: '開始日と終了日を指定',
table: {
type: {
summary: 'DateRange',
detail: '{ startDate: Date, endDate: Date }',
},
},
};
export const AnalyticsChart: Story = {
argTypes: {
dateRange: dateRangeControl,
chartType: {
control: 'select',
options: ['line', 'bar', 'pie'],
description: 'チャートの種類',
},
},
args: {
dateRange: {
startDate: new Date('2024-01-01'),
endDate: new Date('2024-12-31'),
},
chartType: 'line',
},
};
実用性の高いポイント
機能 | 従来の方法 | Advanced Controls |
---|---|---|
オブジェクト編集 | コード変更が必要 | GUI で直接編集可能 |
デバッグ速度 | 変更 → 保存 → 確認のサイクル | リアルタイム反映 |
非エンジニア確認 | 困難 | 直感的に操作可能 |
状態の再現 | 複雑 | URL で状態共有可能 |
ベストプラクティス 2:Mock Service Worker で API モックを完全再現
実際のプロダクトでは、多くのコンポーネントが API との連携を前提としています。MSW(Mock Service Worker)を活用することで、本物の API レスポンスを完全に再現できます。
MSW の基本セットアップ
bash# MSW のインストール
yarn add -D msw
yarn add -D @storybook/addon-msw-storybook
typescript// .storybook/main.ts
export default {
addons: [
'@storybook/addon-essentials',
'@storybook/addon-msw-storybook', // MSW アドオンを追加
],
};
typescript// src/mocks/handlers.ts
import { rest } from 'msw';
export const handlers = [
// ユーザー一覧 API のモック
rest.get('/api/users', (req, res, ctx) => {
const page = req.url.searchParams.get('page') || '1';
const limit = req.url.searchParams.get('limit') || '10';
return res(
ctx.status(200),
ctx.json({
users: Array.from(
{ length: parseInt(limit) },
(_, i) => ({
id:
(parseInt(page) - 1) * parseInt(limit) +
i +
1,
name: `ユーザー ${
(parseInt(page) - 1) * parseInt(limit) + i + 1
}`,
email: `user${
(parseInt(page) - 1) * parseInt(limit) + i + 1
}@example.com`,
status: i % 3 === 0 ? 'active' : 'inactive',
})
),
total: 100,
page: parseInt(page),
totalPages: Math.ceil(100 / parseInt(limit)),
})
);
}),
// エラーレスポンスのモック
rest.get('/api/users/error', (req, res, ctx) => {
return res(
ctx.status(500),
ctx.json({
error: 'Internal Server Error',
message: 'データベース接続エラーが発生しました',
})
);
}),
];
React Query との連携パターン
typescript// UserList コンポーネント(React Query 使用)
import { useQuery } from '@tanstack/react-query';
interface User {
id: number;
name: string;
email: string;
status: 'active' | 'inactive';
}
interface UserListProps {
page?: number;
limit?: number;
}
export const UserList: React.FC<UserListProps> = ({
page = 1,
limit = 10,
}) => {
const { data, isLoading, error } = useQuery({
queryKey: ['users', page, limit],
queryFn: async () => {
const response = await fetch(
`/api/users?page=${page}&limit=${limit}`
);
if (!response.ok)
throw new Error('Failed to fetch users');
return response.json();
},
});
if (isLoading)
return <div className='loading'>読み込み中...</div>;
if (error)
return (
<div className='error'>エラーが発生しました</div>
);
return (
<div className='user-list'>
<h2>ユーザー一覧</h2>
<div className='users'>
{data?.users.map((user: User) => (
<div
key={user.id}
className={`user user--${user.status}`}
>
<h3>{user.name}</h3>
<p>{user.email}</p>
<span className='status'>{user.status}</span>
</div>
))}
</div>
<div className='pagination'>
{data?.page} / {data?.totalPages} ページ
</div>
</div>
);
};
Storybook での MSW 活用
typescript// UserList.stories.ts
import {
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query';
import { UserList } from './UserList';
import { handlers } from '../mocks/handlers';
const meta: Meta<typeof UserList> = {
title: 'API/UserList',
component: UserList,
decorators: [
(Story) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
return (
<QueryClientProvider client={queryClient}>
<Story />
</QueryClientProvider>
);
},
],
};
export default meta;
type Story = StoryObj<typeof meta>;
// 正常なレスポンスパターン
export const Default: Story = {
parameters: {
msw: {
handlers: [handlers[0]], // 正常レスポンスのハンドラー
},
},
args: {
page: 1,
limit: 5,
},
};
// エラーレスポンスパターン
export const ErrorState: Story = {
parameters: {
msw: {
handlers: [handlers[1]], // エラーレスポンスのハンドラー
},
},
args: {
page: 1,
limit: 5,
},
};
// 遅延レスポンスパターン
export const SlowResponse: Story = {
parameters: {
msw: {
handlers: [
rest.get('/api/users', (req, res, ctx) => {
return res(
ctx.delay(3000), // 3秒の遅延
ctx.status(200),
ctx.json({
users: [],
total: 0,
page: 1,
totalPages: 1,
})
);
}),
],
},
},
};
MSW 活用の利点
- 本物に近い動作確認: 実際の HTTP リクエストをインターセプト
- エラーケースのテスト: 様々なエラーレスポンスを簡単に再現
- ネットワーク遅延の再現: 遅い通信環境での動作確認
- API 仕様の文書化: モックが API の仕様書としても機能
ベストプラクティス 3:Composition でコンポーネントの組み合わせテスト
実際のアプリケーションでは、複数のコンポーネントが組み合わさって動作します。Composition パターンを活用することで、コンポーネント間の連携を効果的にテストできます。
複合コンポーネントの設計
typescript// ショッピングカートの例
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
image: string;
}
interface CartContextType {
items: CartItem[];
addItem: (item: Omit<CartItem, 'quantity'>) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
total: number;
}
const CartContext =
React.createContext<CartContextType | null>(null);
// カートプロバイダー
export const CartProvider: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
const [items, setItems] = useState<CartItem[]>([]);
const addItem = (item: Omit<CartItem, 'quantity'>) => {
setItems((prev) => {
const existing = prev.find((i) => i.id === item.id);
if (existing) {
return prev.map((i) =>
i.id === item.id
? { ...i, quantity: i.quantity + 1 }
: i
);
}
return [...prev, { ...item, quantity: 1 }];
});
};
const removeItem = (id: string) => {
setItems((prev) =>
prev.filter((item) => item.id !== id)
);
};
const updateQuantity = (id: string, quantity: number) => {
if (quantity === 0) {
removeItem(id);
return;
}
setItems((prev) =>
prev.map((item) =>
item.id === id ? { ...item, quantity } : item
)
);
};
const total = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return (
<CartContext.Provider
value={{
items,
addItem,
removeItem,
updateQuantity,
total,
}}
>
{children}
</CartContext.Provider>
);
};
// 商品カード
export const ProductCard: React.FC<{
product: Omit<CartItem, 'quantity'>;
}> = ({ product }) => {
const cart = useContext(CartContext);
return (
<div className='product-card'>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p className='price'>
¥{product.price.toLocaleString()}
</p>
<button onClick={() => cart?.addItem(product)}>
カートに追加
</button>
</div>
);
};
// カート表示
export const CartSummary: React.FC = () => {
const cart = useContext(CartContext);
if (!cart?.items.length) {
return <div className='cart-empty'>カートは空です</div>;
}
return (
<div className='cart-summary'>
<h3>カート内容</h3>
{cart.items.map((item) => (
<div key={item.id} className='cart-item'>
<span>{item.name}</span>
<div className='quantity-controls'>
<button
onClick={() =>
cart.updateQuantity(
item.id,
item.quantity - 1
)
}
>
-
</button>
<span>{item.quantity}</span>
<button
onClick={() =>
cart.updateQuantity(
item.id,
item.quantity + 1
)
}
>
+
</button>
</div>
<span>
¥{(item.price * item.quantity).toLocaleString()}
</span>
</div>
))}
<div className='total'>
合計: ¥{cart.total.toLocaleString()}
</div>
</div>
);
};
Composition ストーリーの作成
typescript// ShoppingExperience.stories.ts
import {
CartProvider,
ProductCard,
CartSummary,
} from './ShoppingComponents';
const sampleProducts = [
{
id: '1',
name: 'ワイヤレスヘッドフォン',
price: 15000,
image: '/images/headphones.jpg',
},
{
id: '2',
name: 'スマートウォッチ',
price: 35000,
image: '/images/smartwatch.jpg',
},
{
id: '3',
name: 'Bluetooth スピーカー',
price: 8000,
image: '/images/speaker.jpg',
},
];
const meta: Meta = {
title: 'Composition/ShoppingExperience',
decorators: [
(Story) => (
<CartProvider>
<div
style={{
display: 'grid',
gridTemplateColumns: '2fr 1fr',
gap: '24px',
}}
>
<Story />
</div>
</CartProvider>
),
],
};
export default meta;
// 完全なショッピング体験
export const CompleteExperience: StoryObj = {
render: () => (
<>
<div>
<h2>商品一覧</h2>
<div
style={{
display: 'grid',
gridTemplateColumns:
'repeat(auto-fit, minmax(250px, 1fr))',
gap: '16px',
}}
>
{sampleProducts.map((product) => (
<ProductCard
key={product.id}
product={product}
/>
))}
</div>
</div>
<div>
<CartSummary />
</div>
</>
),
parameters: {
docs: {
description: {
story:
'商品の追加からカート確認まで、完全なユーザー体験を再現',
},
},
},
};
// 異なる状態での表示確認
export const PreFilledCart: StoryObj = {
render: () => (
<>
<div>
<h2>商品一覧</h2>
<div
style={{
display: 'grid',
gridTemplateColumns:
'repeat(auto-fit, minmax(250px, 1fr))',
gap: '16px',
}}
>
{sampleProducts.map((product) => (
<ProductCard
key={product.id}
product={product}
/>
))}
</div>
</div>
<div>
<CartSummary />
</div>
</>
),
decorators: [
(Story) => {
// 初期状態でカートにアイテムを追加
const CartWithInitialItems: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
const [items, setItems] = useState<CartItem[]>([
{ ...sampleProducts[0], quantity: 2 },
{ ...sampleProducts[1], quantity: 1 },
]);
// 実際のCartProviderロジックを実装...
return <CartProvider>{children}</CartProvider>;
};
return (
<CartWithInitialItems>
<div
style={{
display: 'grid',
gridTemplateColumns: '2fr 1fr',
gap: '24px',
}}
>
<Story />
</div>
</CartWithInitialItems>
);
},
],
};
Composition の利点
側面 | 単体テスト | Composition テスト |
---|---|---|
現実性 | 人工的な環境 | 実際の使用状況に近い |
インタラクション | 限定的 | 複数コンポーネント間の連携 |
バグ発見 | コンポーネント内部 | 統合時の問題 |
ユーザー体験 | 部分的 | エンドツーエンド |
ベストプラクティス 4:Visual Testing の自動化で回帰バグを撲滅
UI の変更は意図しない視覚的な回帰バグを引き起こしがちです。Visual Testing を自動化することで、これらの問題を効率的に検出できます。
Chromatic との連携
bash# Chromatic のセットアップ
yarn add -D chromatic
typescript// package.json にスクリプト追加
{
"scripts": {
"chromatic": "chromatic --project-token=YOUR_PROJECT_TOKEN",
"chromatic:ci": "chromatic --project-token=YOUR_PROJECT_TOKEN --exit-zero-on-changes"
}
}
typescript// .storybook/main.ts
export default {
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-essentials',
{
name: '@storybook/addon-visual-tests', // Visual Testing アドオン
options: {
service: 'chromatic',
},
},
],
};
レグレッションテスト対応ストーリー
typescript// Button.stories.ts - Visual Testing に最適化
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Visual/Button',
component: Button,
parameters: {
// 一貫したスクリーンショットのための設定
layout: 'centered',
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#333333' },
],
},
},
};
export default meta;
type Story = StoryObj<typeof meta>;
// すべての状態を一度に確認
export const AllStates: Story = {
render: () => (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 200px)',
gap: '16px',
padding: '16px',
}}
>
{/* 通常状態 */}
<div style={{ textAlign: 'center' }}>
<h4>Primary</h4>
<Button variant='primary'>Button</Button>
</div>
{/* ホバー状態 */}
<div style={{ textAlign: 'center' }}>
<h4>Primary Hover</h4>
<Button variant='primary' className='hover'>
Button
</Button>
</div>
{/* フォーカス状態 */}
<div style={{ textAlign: 'center' }}>
<h4>Primary Focus</h4>
<Button variant='primary' className='focus'>
Button
</Button>
</div>
{/* 無効状態 */}
<div style={{ textAlign: 'center' }}>
<h4>Disabled</h4>
<Button variant='primary' disabled>
Button
</Button>
</div>
{/* Secondary バリエーション */}
<div style={{ textAlign: 'center' }}>
<h4>Secondary</h4>
<Button variant='secondary'>Button</Button>
</div>
{/* ローディング状態 */}
<div style={{ textAlign: 'center' }}>
<h4>Loading</h4>
<Button variant='primary' loading>
Button
</Button>
</div>
{/* サイズバリエーション */}
<div style={{ textAlign: 'center' }}>
<h4>Small</h4>
<Button variant='primary' size='small'>
Button
</Button>
</div>
<div style={{ textAlign: 'center' }}>
<h4>Large</h4>
<Button variant='primary' size='large'>
Button
</Button>
</div>
</div>
),
parameters: {
// Visual Testing 専用の設定
chromatic: {
disableSnapshot: false,
pauseAnimationAtEnd: true,
},
},
};
// ダークテーマでの表示確認
export const DarkTheme: Story = {
...AllStates,
parameters: {
backgrounds: { default: 'dark' },
chromatic: {
disableSnapshot: false,
},
},
};
// レスポンシブ対応の確認
export const ResponsiveLayout: Story = {
render: () => (
<div>
{/* デスクトップ */}
<div
style={{ width: '1200px', marginBottom: '32px' }}
>
<h3>Desktop (1200px)</h3>
<div style={{ display: 'flex', gap: '12px' }}>
<Button variant='primary'>Primary</Button>
<Button variant='secondary'>Secondary</Button>
<Button variant='outline'>Outline</Button>
</div>
</div>
{/* タブレット */}
<div style={{ width: '768px', marginBottom: '32px' }}>
<h3>Tablet (768px)</h3>
<div style={{ display: 'flex', gap: '12px' }}>
<Button variant='primary'>Primary</Button>
<Button variant='secondary'>Secondary</Button>
<Button variant='outline'>Outline</Button>
</div>
</div>
{/* モバイル */}
<div style={{ width: '375px' }}>
<h3>Mobile (375px)</h3>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '12px',
}}
>
<Button variant='primary' fullWidth>
Primary
</Button>
<Button variant='secondary' fullWidth>
Secondary
</Button>
<Button variant='outline' fullWidth>
Outline
</Button>
</div>
</div>
</div>
),
parameters: {
chromatic: {
viewports: [1200, 768, 375],
},
},
};
GitHub Actions との統合
yaml# .github/workflows/chromatic.yml
name: Visual Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
chromatic:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0 # Chromatic に必要
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run Chromatic
uses: chromaui/action@v1
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
onlyChanged: true # 変更されたストーリーのみテスト
skipUpdateCheck: true
Visual Testing の効果
従来の手動確認 | 自動 Visual Testing |
---|---|
確認漏れが発生しやすい | 全パターンを自動チェック |
時間がかかる | 数分で完了 |
主観的な判断 | 客観的なピクセル比較 |
ブラウザ依存 | 複数環境で一貫した結果 |
ベストプラクティス 5:CSF3.0 を活用した型安全なストーリー設計
Component Story Format 3.0(CSF3.0)の新機能を活用することで、より型安全で保守しやすいストーリーを作成できます。
オブジェクト記法から関数記法への移行
typescript// 従来のCSF2.0記法
export const OldStyle = {
args: {
variant: 'primary',
children: 'Old Style Button',
},
};
// CSF3.0の関数記法
export const NewStyle: Story = {
args: {
variant: 'primary',
children: 'New Style Button',
},
// render関数で完全な制御
render: (args) => {
return (
<div style={{ padding: '20px' }}>
<Button {...args} />
</div>
);
},
};
型安全な引数の定義
typescript// 型安全なストーリー設計
import type { Meta, StoryObj } from '@storybook/react';
import { expect, userEvent, within } from '@storybook/test';
interface FormComponentProps {
onSubmit: (data: FormData) => void;
initialValues?: Partial<FormData>;
validationRules?: ValidationRules;
isLoading?: boolean;
}
interface FormData {
email: string;
password: string;
rememberMe: boolean;
}
const meta: Meta<FormComponentProps> = {
title: 'Forms/LoginForm',
component: LoginForm,
parameters: {
layout: 'centered',
},
// 引数の型を厳密に定義
argTypes: {
onSubmit: { action: 'submitted' },
initialValues: {
control: 'object',
description: 'フォームの初期値',
},
isLoading: {
control: 'boolean',
description: 'ローディング状態',
},
} satisfies Meta<FormComponentProps>['argTypes'],
};
export default meta;
type Story = StoryObj<typeof meta>;
// 基本的なフォーム
export const Default: Story = {
args: {
onSubmit: (data) => {
console.log('Form submitted:', data);
},
},
};
// バリデーションエラーの状態
export const WithValidationErrors: Story = {
args: {
initialValues: {
email: 'invalid-email',
password: '123',
},
},
render: (args) => {
const [errors, setErrors] = React.useState<
Record<string, string>
>({});
const handleSubmit = (data: FormData) => {
const newErrors: Record<string, string> = {};
if (!data.email.includes('@')) {
newErrors.email =
'有効なメールアドレスを入力してください';
}
if (data.password.length < 6) {
newErrors.password =
'パスワードは6文字以上で入力してください';
}
setErrors(newErrors);
if (Object.keys(newErrors).length === 0) {
args.onSubmit?.(data);
}
};
return (
<LoginForm
{...args}
onSubmit={handleSubmit}
errors={errors}
/>
);
},
parameters: {
docs: {
description: {
story:
'バリデーションエラーが発生した場合の表示を確認できます。',
},
},
},
};
// インタラクションテスト付きストーリー
export const InteractiveTest: Story = {
args: {
onSubmit: (data) => {
console.log('Submitted:', data);
},
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
// メールアドレス入力
const emailInput =
canvas.getByLabelText(/メールアドレス/i);
await userEvent.type(emailInput, 'test@example.com');
// パスワード入力
const passwordInput =
canvas.getByLabelText(/パスワード/i);
await userEvent.type(passwordInput, 'password123');
// チェックボックスをクリック
const checkbox =
canvas.getByLabelText(/ログイン状態を保持/i);
await userEvent.click(checkbox);
// フォーム送信
const submitButton = canvas.getByRole('button', {
name: /ログイン/i,
});
await userEvent.click(submitButton);
// 送信されたことを確認
await expect(args.onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
rememberMe: true,
});
},
};
条件付きレンダリングの型安全な実装
typescript// 複雑な条件分岐を型安全に
export const ConditionalRendering: Story = {
args: {
userRole: 'admin',
permissions: ['read', 'write', 'delete'],
},
render: (args) => {
const { userRole, permissions } = args;
// 型ガードを活用
const hasPermission = (permission: string): boolean => {
return permissions?.includes(permission) ?? false;
};
if (userRole === 'admin') {
return (
<AdminDashboard
permissions={permissions}
onAction={(action) =>
console.log('Admin action:', action)
}
/>
);
}
if (userRole === 'user' && hasPermission('read')) {
return (
<UserDashboard
readOnly={!hasPermission('write')}
onAction={(action) =>
console.log('User action:', action)
}
/>
);
}
return <AccessDenied />;
},
argTypes: {
userRole: {
control: 'select',
options: ['admin', 'user', 'guest'] as const,
},
permissions: {
control: 'check',
options: [
'read',
'write',
'delete',
'admin',
] as const,
},
},
};
CSF3.0 の利点
従来(CSF2.0) | CSF3.0 |
---|---|
型チェックが不完全 | 完全な型安全性 |
render 関数の制約 | 柔軟なレンダリング制御 |
テストとの統合が困難 | play 関数で完全なテスト |
再利用性が低い | コンポーザブルな設計 |
ベストプラクティス 6:Performance 測定でコンポーネント最適化
パフォーマンス問題は往々にして見落とされがちですが、Storybook でパフォーマンス測定を組み込むことで、問題を早期に発見できます。
React DevTools Profiler との連携
typescript// Performance測定用のWrapper
import { Profiler, ProfilerOnRenderCallback } from 'react';
const PerformanceWrapper: React.FC<{
children: React.ReactNode;
id: string;
onRender?: ProfilerOnRenderCallback;
}> = ({ children, id, onRender }) => {
const defaultOnRender: ProfilerOnRenderCallback = (
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) => {
console.group(`🔍 Performance Report: ${id}`);
console.log('Phase:', phase);
console.log(
'Actual Duration:',
`${actualDuration.toFixed(2)}ms`
);
console.log(
'Base Duration:',
`${baseDuration.toFixed(2)}ms`
);
console.log('Start Time:', `${startTime.toFixed(2)}ms`);
console.log(
'Commit Time:',
`${commitTime.toFixed(2)}ms`
);
console.groupEnd();
};
return (
<Profiler
id={id}
onRender={onRender || defaultOnRender}
>
{children}
</Profiler>
);
};
パフォーマンステスト用ストーリー
typescript// PerformanceTest.stories.ts
export const LargeDatasetComparison: Story = {
render: () => {
const [dataSize, setDataSize] = useState(100);
const data = generateLargeDataset(dataSize);
return (
<div>
<div style={{ marginBottom: '16px' }}>
<label>
データサイズ: {dataSize}件
<input
type='range'
min='100'
max='10000'
step='100'
value={dataSize}
onChange={(e) =>
setDataSize(parseInt(e.target.value))
}
/>
</label>
</div>
<PerformanceWrapper id='DataTable'>
<DataTable data={data} />
</PerformanceWrapper>
</div>
);
},
parameters: {
docs: {
description: {
story:
'データサイズを変更してパフォーマンスの変化を測定できます。',
},
},
},
};
ベストプラクティス 7:Custom Addon 開発でチーム独自の機能追加
チーム固有のニーズに対応するため、カスタムアドオンを開発することで Storybook の機能を拡張できます。
デザイントークン表示アドオン
typescript// addon/Panel.tsx
import React from 'react';
import {
useGlobals,
useStorybookApi,
} from '@storybook/api';
import { AddonPanel, Button } from '@storybook/components';
export const Panel: React.FC = () => {
const api = useStorybookApi();
const [globals] = useGlobals();
const designTokens = [
{
name: 'primary-blue',
value: '#3b82f6',
type: 'color',
},
{ name: 'spacing-md', value: '16px', type: 'spacing' },
{
name: 'font-body',
value: '16px/1.5 sans-serif',
type: 'typography',
},
];
const copyToClipboard = (value: string) => {
navigator.clipboard.writeText(value);
api.addNotification({
id: 'token-copied',
type: 'success',
content: `コピーしました: ${value}`,
});
};
return (
<AddonPanel>
<div style={{ padding: '16px' }}>
<h3>デザイントークン</h3>
{designTokens.map((token) => (
<div
key={token.name}
style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
padding: '8px',
borderBottom: '1px solid #e5e7eb',
}}
>
<div
style={{
width: '40px',
height: '40px',
backgroundColor:
token.type === 'color'
? token.value
: '#e5e7eb',
borderRadius: '4px',
}}
/>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: '500' }}>
{token.name}
</div>
<div
style={{
fontSize: '12px',
fontFamily: 'monospace',
}}
>
{token.value}
</div>
</div>
<Button
size='small'
onClick={() => copyToClipboard(token.value)}
>
コピー
</Button>
</div>
))}
</div>
</AddonPanel>
);
};
ベストプラクティス 8:Figma 連携でデザイナーとの完璧な同期
デザインツールとの連携により、デザイナーとエンジニアの間の認識齟齬を最小限に抑えることができます。
Figma Embed アドオンの活用
bashyarn add -D storybook-addon-designs
typescript// Button.stories.ts - Figmaデザインとの連携
export const WithFigmaDesign: Story = {
args: {
variant: 'primary',
children: 'Figma Design Button',
},
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/ABC123/Design-System?node-id=123%3A456',
},
docs: {
description: {
story: 'Figmaデザインと実装の比較ができます。',
},
},
},
};
デザインシステム同期の自動化
typescript// scripts/sync-figma-tokens.ts
async function syncDesignTokens() {
try {
// Figma APIからデザイントークンを取得
const figmaTokens = await figmaApi.getDesignTokens();
// CSS変数として出力
const cssVars = figmaTokens
.map((token) => ` --${token.name}: ${token.value};`)
.join('\n');
const cssContent = `:root {\n${cssVars}\n}`;
await fs.writeFile(
'./src/tokens/figma-tokens.css',
cssContent
);
console.log('✅ Figmaトークンの同期が完了しました');
} catch (error) {
console.error('❌ 同期エラー:', error);
}
}
ベストプラクティス 9:CI/CD パイプラインでの自動デプロイ最適化
継続的な開発において、Storybook の自動デプロイとテストを効率化することが重要です。
GitHub Actions でのデプロイパイプライン
yaml# .github/workflows/storybook.yml
name: Build and Deploy Storybook
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build Storybook
run: yarn build-storybook --quiet
- name: Test Storybook
run: |
npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
"npx http-server storybook-static --port 6006 --silent" \
"npx wait-on http://localhost:6006 && yarn test-storybook"
- name: Run Visual Tests
if: github.event_name == 'push'
run: yarn chromatic --project-token=${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Deploy to GitHub Pages
if: github.ref == 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./storybook-static
ベストプラクティス 10:大規模プロジェクトでのモノレポ運用術
複数のプロジェクトを抱える組織では、モノレポでの効率的な Storybook 運用が重要になります。
統合 Storybook の構築
typescript// apps/storybook-hub/.storybook/main.ts
export default {
stories: [
'../../../libs/design-system/src/**/*.stories.@(js|jsx|ts|tsx)',
'../../../libs/app-components/src/**/*.stories.@(js|jsx|ts|tsx)',
'../../../apps/web-app/src/**/*.stories.@(js|jsx|ts|tsx)',
],
addons: ['@storybook/addon-essentials'],
};
共通設定の管理
typescript// libs/storybook-config/src/preview.ts
export const commonDecorators = [
(Story) => (
<div style={{ margin: '3em' }}>
<Story />
</div>
),
];
export const commonParameters = {
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#333333' },
],
},
};
モノレポ運用の利点
従来の分離型 | モノレポ |
---|---|
依存関係の同期が困難 | 一元的な依存関係管理 |
重複したコンポーネント | 共通コンポーネントの再利用 |
バージョン管理が複雑 | 統一されたバージョン管理 |
CI/CD パイプラインが分散 | 効率的な一括ビルド |
まとめ
今回ご紹介した 10 のベストプラクティスは、いずれも実際の開発現場で培われた実践的なテクニックです。これらを活用することで、Storybook が単なる「コンポーネントカタログ」を超えた、開発チーム全体の生産性を向上させる強力なツールへと進化します。
実践のポイント
段階的な導入がカギ すべてのベストプラクティスを一度に導入する必要はありません。チームの状況と課題に応じて、優先度の高いものから段階的に取り入れていくことが成功の秘訣です。
導入優先度 | ベストプラクティス | 効果 |
---|---|---|
高 | Advanced Controls、MSW、Visual Testing | 即座に開発効率向上 |
中 | CSF3.0、Performance 測定、Figma 連携 | 品質と連携の改善 |
低 | Custom Addon、CI/CD 最適化、モノレポ | 大規模運用での威力発揮 |
チーム全体での合意形成 技術的な導入だけでなく、デザイナー、プロジェクトマネージャーを含めたチーム全体での理解と合意が重要です。特に Figma 連携や Visual Testing は、非エンジニアメンバーにとっても大きなメリットがあります。
継続的な改善 これらのプラクティスは「導入して終わり」ではありません。チームの成長とプロジェクトの変化に合わせて、継続的に改善し続けることが重要です。
効果的な活用のための Tips
- 小さく始める: まずは最も頻繁に使うコンポーネントから高度な機能を適用
- 測定する: パフォーマンスや効率性の変化を数値で把握
- 共有する: チーム内でベストプラクティスの知見を積極的に共有
- 文書化する: 独自のルールやカスタマイズ内容を必ず文書化
最後に
現代の Web 開発において、Storybook は単なるツールを超えた「開発文化」の一部となっています。これらのベストプラクティスを通じて、あなたのチームも効率的で楽しい開発体験を実現できることでしょう。
特に印象的なのは、これらの高度な機能により、エンジニアだけでなく、デザイナーやプロジェクトマネージャーまでもが開発プロセスに積極的に参加できるようになることです。これは、チーム全体の創造性と生産性を大幅に向上させる革新的な変化と言えるでしょう。
ぜひ今日から、一つずつでも実践してみてください。きっと、開発の質とスピードの両方で大きな改善を実感していただけるはずです。
関連リンク
- blog
「アジャイルコーチ」って何する人?チームを最強にする影の立役者の役割と、あなたがコーチになるための道筋
- blog
ペアプロって本当に効果ある?メリットだけじゃない、現場で感じたリアルな課題と乗り越え方
- blog
TDDって結局何がいいの?コードに自信が持てる、テスト駆動開発のはじめの一歩
- blog
「昨日やったこと、今日やること」の報告会じゃない!デイリースクラムをチームのエンジンにするための3つの問いかけ
- blog
燃え尽きるのは誰だ?バーンダウンチャートでプロジェクトの「ヤバさ」をチームで共有する方法
- blog
「誰が、何を、なぜ」が伝わらないユーザーストーリーは無意味。開発者が本当に欲しいストーリーの書き方