背景
Reactアプリの画面をStorybookで表示するとき、アプリ本体で提供しているルーターやレイアウトが必要になった。
アプリはcreateBrowserRouterとRouterProviderを使用し、共通レイアウトにはOutletがある。画面やヘッダーではuseNavigate、useMatchesなどを使う。
Storybook用にcreateMemoryRouterでルートを用意する方針をメモしておく。
コード例の前提
- React 18系
- React Router 6系
- Storybook 8系の
@storybook/react-vite
以下は説明用の最小構成。アプリ固有の認証処理やAPI呼び出しは含めない。
Storybookにもルート構造を用意する
// withAppLayoutForStorybook.tsx
import type { Decorator } from '@storybook/react';
import {
createMemoryRouter,
RouterProvider,
} from 'react-router-dom';
import { AppLayout } from './AppLayout';
export const withAppLayoutForStorybook: Decorator = (Story) => {
const router = createMemoryRouter(
[
{
path: '/',
element: <AppLayout />,
children: [
{
path: 'stock',
element: <Story />,
handle: { title: '在庫検索' },
},
],
},
],
{ initialEntries: ['/stock'] }
);
return <RouterProvider router={router} />;
};
AppLayoutのexport形式と、handleのプロパティ名は実装に合わせる。この例では名前付きexportとhandle.titleを想定している。
この最小例ではデコレーターの再実行時にルーターを作り直すため、Controlsの変更などで遷移状態が初期化される。遷移状態を保持する必要があれば、ルーターの保持方法を別途設計する。
React Router公式では、createMemoryRouterはStorybookなどの環境にも使えるメモリ上のルーターとして説明されている。
Story側で指定する
import type { Meta, StoryObj } from '@storybook/react';
import { StockSearch } from './StockSearch';
import { withAppLayoutForStorybook } from './withAppLayoutForStorybook';
const meta = {
title: 'Pages/StockSearch',
component: StockSearch,
decorators: [withAppLayoutForStorybook],
} satisfies Meta<typeof StockSearch>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
この例もコンポーネントのexport形式に合わせて変更する。
確認すること
- レイアウト内に
Outletがあるか -
initialEntriesとルート定義が一致しているか - ヘッダーが参照する
handleを設定しているか - 画面の遷移先が必要なら、そのルートも定義しているか
- ReduxやDialogProviderなど、ほかのProviderが必要か
- Controlsでargsを変えたときに表示が更新されるか
アプリ本体のルート定義をそのまま読み込むと、ヘルスチェックや認証、API通信まで動くことがある。画面の確認に必要な構成を決めてから再利用する。
このサンプルは最小構成の例であり、当時のプロジェクトでそのまま動作確認した実装ではない。
次回のためのメモ
ルーターを付けるだけでなく、対象画面が依存するレイアウト・ルート情報・Providerを揃える。