Intro to Storybook
  • はじめに
  • 単純なコンポーネント
  • 複合的なコンポーネント
  • データ
  • 画面
  • デプロイ
  • テスト
  • アドオン
  • まとめ
  • 貢献する
Framework:
ReactReact NativeVueAngularSvelteEmber

画面を作る

コンポーネントをまとめて画面を作りましょう
このコミュニティの翻訳は、最新の Storybook バージョンに対応していません。英語ガイドの最新の変更を適用して、この日本語ガイドの更新にご協力ください。 Pull requests を大歓迎します。.

今までボトムアップ (小さく始めてから複雑性を追加していく) で UI の作成に集中してきました。ボトムアップで作業することで、Storybook で遊びながら、それぞれのコンポーネントを切り離された環境で、それぞれに必要なデータを考えながら開発することができました。サーバーを立ち上げたり、画面を作ったりする必要は全くありませんでした!

この章では Storybook を使用して、コンポーネントを組み合わせて画面を作り、完成度を高めていきます。

繋がれた画面

このアプリケーションはとても単純なので、作る画面は些細なものです。リモート API からデータを取得し、(Redux から自分でデータを取得する) TaskList をラップして、Redux からの error フィールドを追加するだけです。

まず、リモート API に接続して様々な状態 (i.e., error, succeeded) をアプリケーションで扱えるようにするために、Redux ストア (src/lib/store.js 内) をアップデートするところから始めましょう:

src/lib/store.js
/* A simple redux store/actions/reducer implementation.
 * A true app would be more complex and separated into different files.
 */
import {
  configureStore,
  createSlice,
+ createAsyncThunk,
} from '@reduxjs/toolkit';

/*
 * The initial state of our store when the app loads.
 * Usually, you would fetch this from a server. Let's not worry about that now
 */

const TaskBoxData = {
  tasks: [],
  status: "idle",
  error: null,
};

/*
 * Creates an asyncThunk to fetch tasks from a remote endpoint.
 * You can read more about Redux Toolkit's thunks in the docs:
 * https://redux-toolkit.js.org/api/createAsyncThunk
 */
+ export const fetchTasks = createAsyncThunk('todos/fetchTodos', async () => {
+   const response = await fetch(
+     'https://jsonplaceholder.typicode.com/todos?userId=1'
+   );
+   const data = await response.json();
+   const result = data.map((task) => ({
+     id: `${task.id}`,
+     title: task.title,
+     state: task.completed ? 'TASK_ARCHIVED' : 'TASK_INBOX',
+   }));
+   return result;
+ });

/*
 * The store is created here.
 * You can read more about Redux Toolkit's slices in the docs:
 * https://redux-toolkit.js.org/api/createSlice
 */
const TasksSlice = createSlice({
  name: 'taskbox',
  initialState: TaskBoxData,
  reducers: {
    updateTaskState: (state, action) => {
      const { id, newTaskState } = action.payload;
      const task = state.tasks.findIndex((task) => task.id === id);
      if (task >= 0) {
        state.tasks[task].state = newTaskState;
      }
    },
  },
  /*
   * Extends the reducer for the async actions
   * You can read more about it at https://redux-toolkit.js.org/api/createAsyncThunk
   */
+  extraReducers(builder) {
+    builder
+    .addCase(fetchTasks.pending, (state) => {
+      state.status = 'loading';
+      state.error = null;
+      state.tasks = [];
+    })
+    .addCase(fetchTasks.fulfilled, (state, action) => {
+      state.status = 'succeeded';
+      state.error = null;
+      // Add any fetched tasks to the array
+      state.tasks = action.payload;
+     })
+    .addCase(fetchTasks.rejected, (state) => {
+      state.status = 'failed';
+      state.error = "Something went wrong";
+      state.tasks = [];
+    });
+ },
});

// The actions contained in the slice are exported for usage in our components
export const { updateTaskState } = TasksSlice.actions;

/*
 * Our app's store configuration goes here.
 * Read more about Redux's configureStore in the docs:
 * https://redux-toolkit.js.org/api/configureStore
 */
const store = configureStore({
  reducer: {
    taskbox: TasksSlice.reducer,
  },
});

export default store;

リモート API エンドポイントからデータを取得するようにストアを更新し、アプリのさまざまな状態を処理できるように準備したので、InboxScreen.jssrc/components ディレクトリに作成しましょう:

src/components/InboxScreen.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchTasks } from '../lib/store';
import TaskList from './TaskList';

export default function InboxScreen() {
  const dispatch = useDispatch();
  // We're retrieving the error field from our updated store
  const { error } = useSelector((state) => state.taskbox);
  // The useEffect triggers the data fetching when the component is mounted
  useEffect(() => {
    dispatch(fetchTasks());
  }, []);

  if (error) {
    return (
      <div className="page lists-show">
        <div className="wrapper-message">
          <span className="icon-face-sad" />
          <div className="title-message">Oh no!</div>
          <div className="subtitle-message">Something went wrong</div>
        </div>
      </div>
    );
  }
  return (
    <div className="page lists-show">
      <nav>
        <h1 className="title-page">
          <span className="title-wrapper">Taskbox</span>
        </h1>
      </nav>
      <TaskList />
    </div>
  );
}

さらに、App コンポーネントを InboxScreen を描画するように変更します (いずれはルーターにどの画面を表示するか決めてもらいますが、今は気にしないでください):

src/App.js
- import logo from './logo.svg';
- import './App.css';
+ import './index.css';
+ import store from './lib/store';

+ import { Provider } from 'react-redux';
+ import InboxScreen from './components/InboxScreen';

function App() {
  return (
-   <div className="App">
-     <header className="App-header">
-       <img src={logo} className="App-logo" alt="logo" />
-       <p>
-         Edit <code>src/App.js</code> and save to reload.
-       </p>
-       <a
-         className="App-link"
-         href="https://reactjs.org"
-         target="_blank"
-         rel="noopener noreferrer"
-       >
-         Learn React
-       </a>
-     </header>
-   </div>
+   <Provider store={store}>
+     <InboxScreen />
+   </Provider>
  );
}
export default App;
💡 src/App.test.js をアップデートすることを忘れないでください。そうしなければ、テストが通らなくなります。

しかし、面白くなるのは Storybook でストーリーをレンダリングするときです。

前回見たように、TaskList コンポーネントは現在 接続された コンポーネントで、タスクのレンダリングは Redux ストアに依存しています。InboxScreen も接続されたコンポーネントなので、同じように、ストーリーにストアを渡します。以下のように InboxScreen.stories.js でストーリーを設定します:

src/components/InboxScreen.stories.js
import React from 'react';

import InboxScreen from './InboxScreen';
import store from '../lib/store';

import { Provider } from 'react-redux';

export default {
  component: InboxScreen,
  title: 'InboxScreen',
  decorators: [(story) => <Provider store={store}>{story()}</Provider>],
};

const Template = () => <InboxScreen />;

export const Default = Template.bind({});
export const Error = Template.bind({});

私たちは error ストーリーですぐに問題を発見することができます。正しい状態が表示されず、タスクのリストが表示されます。この問題を回避する 1 つの方法は、前章で行ったように各状態に対してモックされたバージョンを提供することです。その代わりに、よく知られた API モッキングライブラリを Storybook アドオンと一緒に使用して、この問題を解決するのに役立てます。

壊れた Inbox 画面の状態

API をモックする

今回のアプリケーションは単純で、リモート API 呼び出しにあまり依存しないので、Mock Service WorkerStorybook's MSW addon を使用することにします。Mock Service Worker は、API モックライブラリです。Service Worker に依存してネットワークリクエストを捕捉し、モックデータをレスポンスします。

Get started section でアプリケーションをセットアップすると、両方のパッケージともインストールされます。あとは、それらを設定しストーリーを更新して使用するのみです。

ターミナルで以下のコマンドを実行し、public フォルダの中にサービスワーカーを生成します。:

yarn init-msw

その後、.storybook/preview.js をアップデートしてそれらを初期化する必要があります:

.storybook/preview.js
import '../src/index.css';

+ // Registers the msw addon
+ import { initialize, mswDecorator } from 'msw-storybook-addon';

+ // Initialize MSW
+ initialize();

+ // Provide the MSW addon decorator globally
+ export const decorators = [mswDecorator];

//👇 Configures Storybook to log the actions( onArchiveTask and onPinTask ) in the UI.
export const parameters = {
  actions: { argTypesRegex: '^on[A-Z].*' },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
};

最後に、InboxScreen のストーリーを更新し、リモート API 呼び出しをモックする parameter を組み込みます:

src/components/InboxScreen.stories.js
import React from 'react';

import InboxScreen from './InboxScreen';
import store from '../lib/store';
+ import { rest } from 'msw';
+ import { MockedState } from './TaskList.stories';
import { Provider } from 'react-redux';

export default {
  component: InboxScreen,
  title: 'InboxScreen',
  decorators: [(story) => <Provider store={store}>{story()}</Provider>],
};

const Template = () => <InboxScreen />;

export const Default = Template.bind({});
+ Default.parameters = {
+   msw: {
+     handlers: [
+       rest.get(
+         'https://jsonplaceholder.typicode.com/todos?userId=1',
+         (req, res, ctx) => {
+           return res(ctx.json(MockedState.tasks));
+         }
+       ),
+     ],
+   },
+ };

export const Error = Template.bind({});
+ Error.parameters = {
+   msw: {
+     handlers: [
+       rest.get(
+         'https://jsonplaceholder.typicode.com/todos?userId=1',
+         (req, res, ctx) => {
+           return res(ctx.status(403));
+         }
+       ),
+     ],
+   },
+ };
補足として、データを下の階層に渡していくことは正当な手法です。GraphQL を使う場合は特に。Chromatic を作る際にはこの手法で 800 以上のストーリーを作成しました。

Storybook で error ストーリーが意図したように動作していることが確認できます。 MSW がリモート API をインターセプトして、適切なレスポンスを返しました。

Interactive stories

これまでで、シンプルなコンポーネントから画面まで、完全に機能するアプリケーションを作り上げ、ストーリーを用いてそれぞれの変更を継続的にテストすることができるようになりました。しかし、新しいストーリーを作るたびに、UI が壊れないように、他のすべてのストーリーを手作業でチェックする必要もあります。これは、とても大変な作業です。

この作業や操作を自動化することはできないのでしょうか?

Storybook の play 関数が可能にしてくれます。play 関数はストーリーのレンダリング後に実行される小さなコードスニペットを含みます。

play 関数はタスクが更新されたときに UI に何が起こるかを検証するのに役立ちます。フレームワークに依存しない DOM API を使用しています。つまり、 play 関数を使って UI を操作し、人間の行動をシミュレートするストーリーを、フロントエンドのフレームワークに関係なく書くことができるのです。

実際に動かしてみましょう!以下のようにして新しく作成された InboxScreen ストーリーを更新し、コンポーネント操作を追加してみましょう:

src/components/InboxScreen.stories.js
import React from 'react';

import InboxScreen from './InboxScreen';

import store from '../lib/store';
import { rest } from 'msw';
import { MockedState } from './TaskList.stories';
import { Provider } from 'react-redux';

+ import {
+  fireEvent,
+  within,
+  waitFor,
+  waitForElementToBeRemoved
+ } from '@storybook/testing-library';

export default {
  component: InboxScreen,
  title: 'InboxScreen',
  decorators: [(story) => <Provider store={store}>{story()}</Provider>],
};

const Template = () => <InboxScreen />;

export const Default = Template.bind({});
Default.parameters = {
  msw: {
    handlers: [
      rest.get(
        'https://jsonplaceholder.typicode.com/todos?userId=1',
        (req, res, ctx) => {
          return res(ctx.json(MockedState.tasks));
        }
      ),
    ],
  },
};

+ Default.play = async ({ canvasElement }) => {
+   const canvas = within(canvasElement);
+   // Waits for the component to transition from the loading state
+   await waitForElementToBeRemoved(await canvas.findByTestId('loading'));
+   // Waits for the component to be updated based on the store
+   await waitFor(async () => {
+     // Simulates pinning the first task
+     await fireEvent.click(canvas.getByLabelText('pinTask-1'));
+     // Simulates pinning the third task
+     await fireEvent.click(canvas.getByLabelText('pinTask-3'));
+   });
+ };

新しく作成したストーリーを確認します。Interactions パネルをクリックすると、ストーリーの play 関数内のインタラクションのリストが表示されます。

play 関数を利用して、UI を操作し、タスクを更新した場合の反応を素早く確認することができます。これによって、余計な手間をかけずに UI の一貫性を保つことができます。テスト環境を立ち上げたり、パッケージを追加したりする必要はありません。

コンポーネント駆動開発

まず、一番下の Task から始めて、TaskList を作り、画面全体の UI が出来ました。InboxScreen では繋がれたコンポーネントを含み、一緒にストーリーも作成しました。

コンポーネント駆動開発 (CDD) はコンポーネント階層を上がるごとに少しずつ複雑性を拡張していきます。利点としては、開発プロセスに集中できること、UI の組み合わせの網羅性を向上できること、が挙げられます。要するに、CDD によって、高品質で複雑な UI を作ることができます。

まだ終わりではありません。UI を作成しても仕事は終わりません。長期間にわたり耐久性を維持できるようにしなければなりません。

💡 Git へのコミットを忘れずに行ってください!
Keep your code in sync with this chapter. View e6e6cae on GitHub.
Is this free guide helping you? Tweet to give kudos and help other devs find it.
Next Chapter
デプロイ
Storybook をインターネット上にデプロイする方法を学びましょう
✍️ Edit on GitHub – PRs welcome!
Docs
Documentation
Add Storybook to your project in less than a minute to build components faster and easier.
reactvueangularweb-components
Tutorial
Tutorials
Learn Storybook with in-depth tutorials that teaches Storybook best practices. Follow along with code samples.
Learn Storybook now
Storybook
The MIT License (MIT). Website design by @domyen and the awesome Storybook community.
StorybookShowcaseDocsTutorialsAddonsBlogReleasesGet involvedUse casesSupportTelemetryTeam
Community GitHub Twitter Discord chat Youtube Component Driven UIs
Subscribe
Get news, free tutorials, and Storybook tips emailed to you.

Maintained by
Chromatic
Continuous integration by
CircleCI
Hosting by
Netlify