27
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Next.jsでの開発にStorybookを導入する

Last updated at Posted at 2022-02-01

はじめに

この記事は、StorybookをNext.jsの開発に導入する流れと、簡単な使い方、そしてStorybookを用いたコンポーネントテストについてまとめた記事です。

・・・と思ったのですが、今回はStorybookをNext.jsの開発に導入する流れと、簡単な使い方まで書いた状態で投稿します。
Storybookを用いたコンポーネントテストについては、後日、別記事を作成したいと思います。

Storybookとは?

Storybookとは、UIコンポーネントのカタログのようなもので、各componentのUIや、渡すpropsに応じた挙動の確認を行うことができます。

開発メンバー間の認識合わせや、同じようなコンポーネントを重複して作成してしまうことの防止などに繋がります。

Storybook導入〜設定

プロジェクト作成

まずは、プロジェクトを作成。
私は今回、Next.js + TypeScriptで開発しているので、以下のように作成しました。
(yarnを用いて作成)

yarn create next-app --typescript [プロジェクト名]

Storybookのインストール

Storybookのインストールは、プロジェクトのルートディレクトリの中で、Storybook CLIを使用して1つのコマンドで行います。

npx sb init

このコマンドを実行することで、Storybookを使用するのに必要なパッケージが自動的にインストールされ、package.jsonのscriptsにも自動で必要なコマンドが追加されます。

(実行後のpackage.jsonのscripts)
scripts
(実行後のpackage.jsonのdevDependencies)
package.json

また、Storybookの設定用ファイルに加え、Storybook使用のサンプルとして、いくつかのコンポーネントとStorybook登録用の設定ファイルが追加されます。

Storybook用リンター設定

Storybookをインストールしていると、 途中で"Do you want to run the 'eslintPlugin' fix on your project?"(eslintのプラグインを入れますか?)と聞かれます。
ESLintの静的解析を行いたい場合、"y"と入力することで”eslint-plugin-storybook”がインストールされます。

eslintrc.jsonに、以下のように追加します。

eslintrc.json
"extends": [
        ....
    "plugin:storybook/recommended",  // 追加

Storybookのmain.js設定

Storybookの全体的な設定は、"npx sb init"実行時に自動的に追加される、.storybook/main.jsの中で行います。

  • stories ...storiesファイル(コンポーネント登録用ファイル)までのファイルパスを指定。
  • addons ...導入したいアドオン(拡張機能)を指定

ちなみに私はTailwind CSSを利用しており、Storybook 起動時に PostCSS DeprecationWarningというものが発生しました。ライブラリが使用している PostCSS version と Storybook のPostCSS versionが異なることで発生するエラーのようです。

そのため、PostCSSのversionの整合性を取る為のアドオンをインストールして設定しています。

.storybook/main.js
module.exports = {
  stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"], // storiesファイルまでのファイルパス
  addons: [
    "@storybook/addon-links",  // デフォルトで設定済み
    "@storybook/addon-essentials", // デフォルトで設定済み
    "@storybook/addon-postcss",   // ⇦ 追加
  ],
  framework: "@storybook/react",
};

Storybookにコンポーネントを登録する

簡単な、コンポーネントを登録(ストーリーを定義というらしい)するまでの流れを記述します。

コンポーネントを作成

まずは、Storybookに登録したいコンポーネントを作成します。
今回は、私が作成した汎用的なボタンを例に挙げたいと思います。Button.tsxファイルは以下のようになっています。

src/components/Button.tsx
type Props = {
  label: string;
  backgroundColor?: string;
  color?: string;
  size?: "xs" | "sm" | "md" | "lg";
  onClick: MouseEventHandler<HTMLButtonElement>;
};

/**
 * 汎用的なボタンコンポーネント.
 */
export const Button: FC<Props> = memo((props) => {
  const {
    label,
    backgroundColor = "#f28728",
    color = "#fff",
    size,
    onClick,
  } = props;

  let scale = 1;
  if (size === "xs") scale = 0.3;
  if (size === "sm") scale = 0.75;
  if (size === "lg") scale = 1.5;

  const style = {
    backgroundColor,
    color,
    padding: `${scale * 0.3}rem ${scale * 1}rem`,
    width: `${scale * 10}rem`,
  };

  return (
    <button
      onClick={onClick}
      style={style}
      className="rounded-md shadow-md border-none font-bold hover:opacity-90"
    >
      {label}
    </button>
  );
});
 

ラベルやカラー、サイズなどをpropsで渡すことで、さまざまな様相をしたボタンになるように作成しています。
backgroundColorにはアプリケーションのテーマカラー、colorには白をデフォルト値として指定しています。

storiesファイル作成(基本)

上で作成したボタンコンポーネントをStorybookに登録するために、storiesファイルを作成します。
今回私は、プロジェクト直下にsrcディレクトリを作成して開発しているのですが、その直下にstoriesディレクトリを作って、その中にstoriesファイルを置くことにしました。

src/componentsの中、コンポーネント作成ファイルと同階層にstoriesファイルを置くことも多いようですが、それではcomponentsディレクトリの中のファイル数が多くなり、見にくくなると思ったからです。

実際に開発コードの中で使うコンポーネントのファイルとは分離させたいと考えました。

Storybookによるストーリーの定義は、.stories.jsx(tsx) という拡張子で定義します。

(全体像)

src/stories/Button.stories.tsx
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { Button } from "../components/Button/Button";

// storyのmetadataをdefault export
export default {
  title: "Button",   // コンポーネントのタイトル(任意)
  component: Button, // 実際に使用するコンポーネント(上でimportしたもの)
} as ComponentMeta<typeof Button>;

/// 1. Storybookで描画するためのコンポーネントの雛形を用意しておく
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;

// 2. bindを用いて雛形を元にしたコピーを作成
// 名前付きエクスポートはデフォルトでストーリーオブジェクトを表す
export const Default: ComponentStory<typeof Button> = Template.bind({});
// 3. Propsに値を設定しない
Default.args = {
  label: "Default",
  onClick: () => {
    alert("クリック");
  },
};
Default.storyName = "デフォルト";

export const SubButton: ComponentStory<typeof Button> = Template.bind({});
SubButton.args = {
  label: "SubButton",
  backgroundColor: "#f6f0ea",
  color: "#622d18",
};
SubButton.storyName = "サブボタン";

export const Small: ComponentStory<typeof Button> = Template.bind({});
Small.args = {
  label: "Small",
  size: "sm",
};
Small.storyName = "小さいボタン";

export const Large: ComponentStory<typeof Button> = Template.bind({});
Large.args = {
  label: "Large",
  size: "lg",
};
Large.storyName = "大きいボタン";

export const SubSmall: ComponentStory<typeof Button> = Template.bind({});
SubSmall.args = {
  label: "SubSmall",
  backgroundColor: "#f6f0ea",
  color: "#622d18",
  size: "sm",
};
SubSmall.storyName = "小さいサブボタン";

  • storyのmetadataを定義してdefault export
src/stories/Button.stories.tsx
export default {
  title: "Button",   // タイトル(任意)。 コンポーネント名と同じにするなら省略してもよさそう。
  component: Button, // 実際に使用するコンポーネント(上でimportしたもの)
} as ComponentMeta<typeof Button>;
    1. Storybookで描画するためのコンポーネントの雛形を用意しておく

argsには、各ストーリーで後々設定するpropsが入ってきます。

src/stories/Button.stories.tsx
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;
    1. bindを用いて雛形を元にしたコピーを作成

storiesファイルで名前付きエクスポートを行うと、デフォルトでストーリーオブジェクトであると認識されます。

src/stories/Button.stories.tsx
export const Default: ComponentStory<typeof Button> = Template.bind({});
    1. 作成したコピーにそれぞれの引数を設定することで、様々なpropsに応じたコンポーネント(今回はボタン)を作り出すことができます。
      以下はデフォルトのボタンを作り出したいので、カラーやサイズを渡していません。
src/stories/Button.stories.tsx
Default.args = {    // 引数(props)を設定  
  label: "Default",
  onClick: () => {
    alert("クリック");
  },
};
Default.storyName = "デフォルト"; 

.storyNameでそのストーリーのタイトルをつけることができます。

  • サブボタンのストーリーも定義してみる
src/stories/Button.stories.tsx
export const SubButton: ComponentStory<typeof Button> = Template.bind({});
SubButton.args = {
  label: "SubButton",
  backgroundColor: "#f6f0ea", // サブで作成したいボタンのカラーを設定
  color: "#622d18",
};
SubButton.storyName = "サブボタン";

Storyの型定義について

型定義ではComponentMeta, ComponentStoryを使います。

src/stories/Button.stories.tsx
import type { ComponentStory, ComponentMeta } from "@storybook/react";
src/stories/Button.stories.tsx
export default {
  title: "Button",   
  component: Button, 
} as ComponentMeta<typeof Button>;
src/stories/Button.stories.tsx
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;

ちょっと前まで(実際に開発でStorybookをいじっていた1ヶ月くらい前)はMeta、Storyというものを使っていましたが、こっちがデフォに変わったらしい。(記事を書くときに知ってコードを直しました。)
公式ドキュメントでもこちらで書かれています。

propsの型も、コンポーネントファイルでexportしてここでimportして、、、ということをしてましたが、上記のように「typeof [コンポーネント名]」で良くなったみたい。

Storybookを起動して確認

上のpackage.jsonのscriptsにもあるように、”yarn storybook”(npmであれば"npm run storybook") とすることで、Storybookの画面を立ち上げることができます。

  • Canvas

ターミナルに出てくるURLをクリックすると、以下のような画面が出てきます。

package.json

左側に登録したコンポーネントの一覧が並び、それを選択することで、コンポーネントのUIを確認することができます。
下のControlsのところをいじると、その場でpropsを変更して、propsでの違いを見ることもできます。

  • Docs

また、上のDocsメニューを押すと、以下のようなコンポーネントごとのドキュメントを見ることができます。

package.json

下の方へ行くと、定義したストーリーが並んでいます。
"Show code"で、その見た目にするにはどのようにpropsを渡せば良いかが一目瞭然なのでとても便利です!

package.json

CSF3.0が正式に使えるように?!

ここまで書いたのは、CSF2.0の話。

CSFとは??

CSF(Component Story Format) : Storybook5.2から推奨の書き方となった、Storybookのフォーマット。それまでstoriesOf APIというものを利用していた、らしい。CSFで書くことで、Jest や Cypress など、他ツールとも連携できるようになったとのこと。

CSF3.0へ

2021年11月末、Storybook6.4が正式にリリースされました。それに伴って、CSFの最新の形式であるCSF3.0が利用可能となったようです。

CSF3.0になることで...

  1. ストーリー定義がより簡潔に記述できるように👏
  2. intercative storyを記述できるように(play関数の登場)👏

ここでは一つ目、「ストーリー定義がより簡潔に記述できるように」という観点で、どのように変わったかを検証したいと思います。

上で、CSF2.0を用いて記述したボタンコンポーネントのストーリーを、CSF3.0に書き換えてみます。

Storybookのバージョンが6.4以上であれば、問題なくCSF3.0を利用することができます。
(私も2.0で書いていましたが、実は6.4以上だったのでCSF3.0で書ける状態でした。)

src/stories/Button.stories.tsx
export default {
  component: Button,
} as ComponentMeta<typeof Button>;

export const Default: ComponentStoryObj<typeof Button> = {
  args: {
    label: "Default",
    onClick: action("clicked!"),
  },
  storyName: "デフォルト",
};

export const SubButton: ComponentStoryObj<typeof Button> = {
  args: {
    label: "SubButton",
    backgroundColor: "#f6f0ea",
    color: "#622d18",
  },
  storyName: "サブボタン",
};

   .
   .
   .

変更点

  • ストーリーが関数からオブジェクトへ

CSF2.0ではコンポーネントの雛形をTemplateとして作成して、各ストーリーでbindを用いてコピー...ということをやっていました。が、3.0ではその冗長な記述が不要になります。

オブジェクト形式で、argsやstoryNameなど、各ストーリーに渡したいものを渡します。

src/stories/Button.stories.tsx
// CSF2.0 ...関数
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;

export const Default: ComponentStory<typeof Button> = Template.bind({});

Default.args = {    
  label: "Default",
  onClick: () => {
    alert("クリック");
  },
};
Default.storyName = "デフォルト"; 

// CSF3.0 ...オブジェクト
export const Default: ComponentStoryObj<typeof Button> = {
  args: {            // propsの定義
    label: "Default",
    onClick: action("clicked!"),
  },
  storyName: "デフォルト", // ストーリーのタイトルを指定
};
  • 型の追加 ComponentStoryObj

ストーリーが関数からオブジェクトに変わったことに伴い、ストーリの型定義として、ComponentStoryObjが追加されました。
2.0でComponentStoryを使っていたところが、ComponentStoryObjに変わるだけです。

play関数の登場

二つ目の「intercative storyを記述できるように(play関数の登場)」については、Storybookでテストを行う際に便利、ということで、別途Storybookでのテストに関する記事の中でまとめたいと思います。

参考文献

  • Storybook公式ドキュメント

  • Next.jsへの導入方法

  • CSF3.0について

27
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?