React

【React】Storybookとは何か?テンプレートを触ってみた

React
この記事は約11分で読めます。

こんにちは。株式会社インプルの下地です。

今回はフロントエンド開発に利用されるライブラリ「Storybook」について簡単に説明します。
また、公式のテンプレートを導入して中身を調べてみました。

Storybookとは

 StoryBookとは、UIコンポーネントとページを切り離し、独立した状態でコンポーネントの開発を行うことができるオープンソースツールです。
ここでいうUIコンポーネントとページの切り離しとは、データやAPIやビジネスロジックに煩わされることなく、コンポーネントとページを実装出来ることを意味します。

 さらに、コンポーネントをカタログのように一覧で見ることができ、それぞれのコンポーネントごとにデザインやテストが可能です。またReact、Vue、Angularなどの主要なJSフレームワークで導入でき幅広く利用することが出来ます。

どうしてStorybookを利用するのか

 現代はWebの発展によってフロントエンドの複雑さが増しており、UIが1つから多数の異なるUIに変化していきました。さらにデバイス、ブラウザ、アクセシビリティ、パフォーマンスや非同期処理などの追加の要件も積み重なってきています。

 プロジェクトによっては多数のコンポーネントが存在し、さらに個別のバリエーションを生み出すこともあります。さらに、これらのUIがビジネスロジックやインタラクティブな状態、アプリのコンテキストと絡み合っているため、デバッグに手間がかかります。開発者は、数え切れないほどのUIのバリエーションを考慮しなければなりませんが、それらをすべて開発したり整理したりする事が難しいという状況になりがちです。

 そこで、UIコンポーネントとページを分離して構築することで上記の問題を解決しました。
コンポーネントの優れた点は、そのレンダリングを確認するためにアプリ全体を起動する必要がないことです。propsを渡したり、モックデータを利用することで、アプリのビジネスロジックやコンテキストに干渉されることなくコンポーネントをレンダリングすることが出来ます。

 Storybookは小さなパッケージとして、コンポーネントの各バリエーションに開発に集中することができます。

Storybookを利用するメリット

Storybookを利用するメリットについて下記の点が挙げられます。

UIコンポーネントとページを分離して構築できるため、アプリを実行せずにUIを開発できます。

・Storybookは、組み込みのワークフローを提供しているため 少ない労力でUIをテストすることが出来ます。

・UIコンポーネントをカタログのように見ることが出来るので、確認や再利用をすぐに行えます。

UIが実際にどのように動作するのかを共有することが可能なので、チーム内での認識を齟齬をなくすことが出来ます。

Storybookについて概ね分かって頂けたと思いますので、実際にStorybookを触ってみましょう。

Storybookの導入

公式のReact 向け Storybook のチュートリアルから実際にStorybookを触ってみます。

公式チュートリアルは日本語なので、もっと詳しく知りたい方は上記チュートリアルを一通り進めてみてください。ここではチュートリアルを抜粋してStorybookの概要を理解していきます。

まずは環境構築を行います。

# テンプレートをクローンする
npx degit chromaui/intro-storybook-react-template taskbox

cd taskbox

# 依存関係をインストール
yarn

yarn完了後、アプリケーションが動くか確認します。Storybookを立ち上げて、左のサイドバーから各ストーリーのコンポーネントを確認することが出来ます。

# Storybookを立ち上げる
yarn storybook

# アプリを立ち上げる
yarn start
Storybookを立ち上げるとコンポーネントの一覧が確認できる
いつもの

「ストーリー」とはコンポーネントの種類のようなもので、一つのコンポーネントに各ストーリーを設定出来ます。

コンポーネント1
 ・ストーリー1
 ・ストーリー2
 ・ストーリー3
イメージ的にはこんな感じ

一つのコンポーネントがあり、そのコンポーネントにそれぞれのストーリーがある(ストーリー1のコンポーネントは赤色、ストーリー2の時は青色、ストーリー3の場合は緑色)イメージです。

Storybookの中身を見てみる

動作確認が完了したので、実際にStorybookがどのような構成になっているか見ていきます。
まずはクローンしてきたtaskboxの直下にある.storybookフォルダを見てみます。

.storybookフォルダ内にはmain.jspreview.jsがありますが、まずmain.jsから見ていきます。

// main.js

module.exports = {
  stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
  staticDirs: ["../public"],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions",
    "@storybook/preset-create-react-app",
  ],
  framework: "@storybook/react",
  core: {
    builder: "@storybook/builder-webpack5",
  },
  features: {
    interactionsDebugger: true,
  },
};

main.jsはStorybookの主な設定ファイルです。
今回作成したテンプレートでは6つの項目があります。

  • stories – ストーリーファイルの場所を示す、main.js からの相対的なパスを記述する場所
  • staticDirs – Storybookで読み込む静的ファイルのディレクトリのリストの設定
  • addons – Storybook によって読み込まれたアドオンのリストの設定
  • framework – フレームワーク固有の設定
  • core – Storybook の内部機能の構成
  • features – Storybook の追加機能の有効化

その他の設定についてはこちらから確認できます。

注意点として、このファイルは Storybookサーバーの動作を制御するため、変更した場合は Storybookのプロセスを再起動する必要があります。

次にpreview.jsを見ていきます。

// preview.js

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
};

preview.jsはエクスポートを介してすべてのストーリーのレンダリングを設定するファイルです。
actionsでは正規表現に一致するすべてのargTypeに対応するアクションを自動的に作成する設定になっています。
controlsも同様に、正規表現に一致したpropTypesまたはargTypesがあった場合にcontrolsと呼ばれる機能によって、コンポーネントなどに与えるデータやプロパティをstorybook上で簡単に変更することができます。

初期状態、backgroundColorは未指定
Controlsからコードを変更せずに自由な色に変えられる

.storybookフォルダにはStorybookの設定ファイルがあるということがわかりました。

次にtaskbox>src>storiesフォルダ内のButton.jsxを見てみます。

// Button.jsx

import React from 'react';
import PropTypes from 'prop-types';
import './button.css';

/**
 * Primary UI component for user interaction
 */
export const Button = ({ primary, backgroundColor, size, label, ...props }) => {
  const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
  return (
    <button
      type="button"
      className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
      style={backgroundColor && { backgroundColor }}
      {...props}
    >
      {label}
    </button>
  );
};

Button.propTypes = {
  /**
   * Is this the principal call to action on the page?
   */
  primary: PropTypes.bool,
  /**
   * What background color to use
   */
  backgroundColor: PropTypes.string,
  /**
   * How large should the button be?
   */
  size: PropTypes.oneOf(['small', 'medium', 'large']),
  /**
   * Button contents
   */
  label: PropTypes.string.isRequired,
  /**
   * Optional click handler
   */
  onClick: PropTypes.func,
};

Button.defaultProps = {
  backgroundColor: null,
  primary: false,
  size: 'medium',
  onClick: undefined,
};

Button.jsxは、UIコンポーネント本体を記述しているファイルです。
通常のコンポーネントと少し異なりButton.propTypesButton.defaultPropsという要素があります。
Button.propTypesはコンポーネントが想定するデータ構造を示します。TypeScriptの型システムのようなものです。Button.defaultPropsButtonコンポーネントの初期値を設定しています。

次にtaskbox>src>storiesButton.stories.jsxを見てみます。

// Button.stories.jsx

import React from 'react';

import { Button } from './Button';

export default {
  title: 'Example/Button',
  component: Button,
  argTypes: {
    backgroundColor: { control: 'color' },
  },
};

const Template = (args) => <Button {...args} />;

export const Primary = Template.bind({});
Primary.args = {
  primary: true,
  label: 'Button',
};

export const Secondary = Template.bind({});
Secondary.args = {
  label: 'Button',
};

export const Large = Template.bind({});
Large.args = {
  size: 'large',
  label: 'Button',
};

export const Small = Template.bind({});
Small.args = {
  size: 'small',
  label: 'Button',
};

Button.stories.jsxButtonコンポーネントの各ストーリーを記述しているファイルです。
Storybookにコンポーネントを反映させるためには、
・title – Storybookのサイドバーにあるコンポーネントを参照する方法
・component – コンポーネントそのもの
この内容を含んだ default exportが必要です。

また、ストーリーを定義するためにテスト用の状態ごとのコンポーネントをエクスポートします。
上記のPrimarySecondaryLargeSmallがストーリーにあたる部分です。またコンポーネント名.argsで各ストーリーごとに引数を渡すことが出来ます。

各ストーリーを単一の Template 変数に割り当てることで、書くべきコードの量が減り、保守性を上げることが出来ます。


まとめ

Storybookについて色々とまとめました。
この記事からStorybookに興味を持ったり理解していただければ幸いです。

この記事で説明したことの他にも様々な機能があるので興味のある方はぜひ公式ドキュメントを読んで実際に触ってみてください。

ここまで読んでいただきありがとうございました。