📖

もう一度学び直すStorybook

2025/02/06に公開

みなさんこんにちは!株式会社アルダグラムでエンジニアをしている大木です。今回は、Storybook を学び直していこうかなと思います。

現在弊社ではStorybookを利用しており、レビューやデザイナーとの連携で活用しています。特に何かに困っているわけでもないのですが、もっとうまく使いこなせないのかなぁと考えていたところでした。
今までは6系を使っていたのですが、とあるチームが8系へのアップデートの作業をしてくださいました(ありがたい限りです)。 そういったキャッチアップのためにも学び直しをしていこうかと思います。

インストール

インストールに関しては以下のページが参考になるかと思います。

https://storybook.js.org/docs/get-started/install

今回は以下で実行します。

npx storybook@latest init

今回は React + Vite の構成でStorybookを作成してみようかと思います。プロジェクトが作成され、Storybookが起動するかと思います。Formatterはbiomeを利用しています。

以下のリポジトリに後ほど記載されているコンポーネントも実装されています。良ければ参考にしてください。
https://github.com/Kazuya-Oki/relearn-storybook

Storybookを利用しながらコンポーネントを作成していく

.stories.js|ts で終わるファイルを作成し、そこでStorybook上で表示させたいコンポーネントをエクスポートします。

https://storybook.js.org/docs/get-started/whats-a-story

雑なデザインですが、今回はプロフィールを表示するためのコンポーネントを作成してみます。

Profile.tsx というファイルを作成してみました。

import { type FunctionComponent, useState } from "react";

type AvatarType = "robot" | "dog" | "duck";
export type ProfileType = {
  /**
   * User Name
   */
  name: string;

  /**
   * Emoji
   */
  avatar: AvatarType;

  /**
   * Number of followers
   */
  follower: number;

  /**
   * Profile color
   */
  backgroundColor: string;

  /**
   * description
   */
  description: string;
};

export const Profile: FunctionComponent<ProfileType> = ({
  name,
  avatar,
  follower,
  backgroundColor,
  description,
}) => {
  const getEmoji = (avatar: AvatarType) => {
    switch (avatar) {
      case "dog":
        return "🐕";
      case "duck":
        return "🦆";
      case "robot":
        return "🤖";
    }
  };

  const [follow, setFollow] = useState(false);

  const followUser = () => setFollow(true);
  return (
    <div
      style={{
        width: "300px",
        border: "1px solid #E6E6E6",
        padding: 20,
        borderRadius: 8,
        backgroundColor: backgroundColor,
        color: "white",
      }}
    >
      <div
        style={{
          display: "flex",
          alignItems: "center",
          lineHeight: 1,
          paddingBottom: 12,
          gap: 16,
        }}
      >
        <div
          style={{
            fontSize: 36,
          }}
        >
          {getEmoji(avatar)}
        </div>
        <p
          style={{
            margin: 0,
            overflow: "hidden",
            whiteSpace: "nowrap",
            textOverflow: "ellipsis",
          }}
        >
          {name}
        </p>
      </div>
      <div>
        <button
          data-testid={"follow-button"}
          style={{
            backgroundColor: follow ? "black" : "white",
            color: follow ? "white" : "black",
            border: "1px solid white",
            borderRadius: 4,
            padding: "4px 8px",
            cursor: "pointer",
          }}
          disabled={follow}
          onClick={followUser}
        >
          {follow ? "フォロー中" : "フォローする"}
        </button>
      </div>
      <div
        style={{
          display: "flex",
          fontSize: 12,
        }}
      >
        <p
          style={{
            margin: 0,
            padding: "8px 0",
          }}
        >
          フォロワー: {follower}
        </p>
      </div>
      <div
        style={{
          fontSize: 12,
          paddingBottom: 4,
          borderBottom: "1px solid white",
        }}
      >
        説明
      </div>
      <p
        style={{
          fontSize: 12,
          paddingTop: 4,
          margin: 0,
        }}
      >
        {description}
      </p>
    </div>
  );
};

作成したプロフィールを表示するコンポーネントを、Storyで表示するために Profile.stories.tsx というファイルを作成します。

import type { Meta, StoryObj } from "@storybook/react";
import { expect, userEvent, within } from "@storybook/test";
import { Profile } from "./Profile";

const meta = {
  title: "Example/Profile",
  component: Profile,
  parameters: {
    layout: "padded",
  },
  tags: ["autodocs"],
  argTypes: {
    follower: {
      type: "number",
      control: {
        min: 1,
        max: 1000000,
      },
    },
  },
  args: {
    name: "name",
  },
} satisfies Meta<typeof Profile>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
  args: {
    name: "大塩 平八郎",
    avatar: "dog",
    follower: 10,
    backgroundColor: "#485353",
    description: "江戸時代の儒学者",
  },
};

export const ClickButton: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    const followButton = canvas.getByTestId("follow-button");
    await userEvent.click(followButton);

    await expect(followButton).toBeDisabled();
  },
  args: {
    name: "大塩 平八郎",
    avatar: "dog",
    follower: 10,
    backgroundColor: "#485353",
    description: "江戸時代の儒学者",
  },
};

Story上では、以下のように表示されるようになるかと思います。

Controlsを使ってpropsを自在に変えてみる

私が今まで使いこなせていなかった部分ではあるのですが…
実装したコンポーネントを色々なバリエーションで確認する際に、Story上で複数パターンを表示するようにコピペで羅列をしていました。
パターンが多いとキリがないしなぁと思っていたところに便利な機能がありました。
それがこちらのControlです。

https://storybook.js.org/docs/essentials/controls

Controlsから値を直接入力することで、実際にどのような見た目になるかを即座に確認ができるようになります。

例えば、長い文字列を表示した場合にどうなるか・Enumのような複数パターンをそれぞれ表示した場合にどうなるかみたいなものを操作者が自在に指定して確認ができるという感じです。
デザイン崩れなどもすぐに気づくことができますし、便利だなと思います。今までStorybookを雰囲気で使っていたんだなと反省するきっかけになりました。

Docsを使ってコメントを表示してみる

上記のコンポーネントの例では既に実装されているのですが、7系から autodocs というものが使えるようになりました。

https://storybook.js.org/blog/storybook-7-docs/#use-jsdoc-for-component-and-story-descriptions

コンポーネント側に記載されているJSDoc形式のコメントを読み取り、それをStory上に表示することができる優れモノです。
今までもStorybookで指定することのできる description というパラメータを利用することで、同じようなことは実現できました。

しかし、コンポーネントを修正する際に併せてStoryも修正する必要があるなど手間がかかっていた部分があります。このアップデートにより、コンポーネント側のコメントだけを変更すれば良くなるため、手間がかかりにくくなったように見受けられます。
Story上で確認するためにも適切にコメントを記載する文化が生まれるようになるため、とても嬉しい改修だなと感じます。

コード上でいうと、以下の部分が該当するものになります。

export type ProfileType = {
  /**
   * User Name
   */
  name: string;

  /**
   * Emoji
   */
  avatar: AvatarType;

  /**
   * Number of followers
   */
  follower: number;

  /**
   * Profile color
   */
  backgroundColor: string;

  /**
   * description
   */
  description: string;
};

コンポーネントのテストを記載する

こちらは既に弊社では取り組んでいるものですが、改めて読み直してみました。

https://storybook.js.org/docs/writing-tests/component-testing

画面の一部分をクリックしたり、プルダウンを選択した後にどのような状態になっているかをテストできるものです。

実際のプロダクトでは、APIをモックしたりなどが必要になるかと思います。今回は、ボタンが押されたら非活性状態になることをテストとして記載しました。

export const ClickButton: Story = {
	play: async ({ canvasElement }) => {
		const canvas = within(canvasElement)

		const followButton = canvas.getByTestId('follow-button')
		await userEvent.click(followButton)

		await expect(followButton).toBeDisabled()
	},
	args: {
		name: "大塩 平八郎",
		avatar: "dog",
		follower: 10,
		backgroundColor: "#485353",
		description: "江戸時代の儒学者",
	},
}

userEvent にはclick以外にも type など色々サポートしているので、コンポーネントのテストで必要なものは充分に網羅できるのではないかと思いました。

https://storybook.js.org/docs/writing-tests/component-testing#api-for-user-events

まとめ

Storybookの簡単なまとめになってしまいましたが、ここまでご覧いただきありがとうございました!
個人的には、autodocs は革新的だなぁ〜と感じました。コメントを記載する・しないに関しては好みが分かれる部分ですが、Storybookで表示されるのであれば適切に記載する文化ができやすくなるかと思いました。
今回はVisual Testについては触れていませんが、 Chromatic との連携もできるため、その辺りも調査してみたいと思います。

アルダグラム Tech Blog

Discussion