はじめに
この記事は、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)
(実行後のpackage.jsonのdevDependencies)
また、Storybookの設定用ファイルに加え、Storybook使用のサンプルとして、いくつかのコンポーネントとStorybook登録用の設定ファイルが追加されます。
Storybook用リンター設定
Storybookをインストールしていると、 途中で"Do you want to run the 'eslintPlugin' fix on your project?"(eslintのプラグインを入れますか?)と聞かれます。
ESLintの静的解析を行いたい場合、"y"と入力することで”eslint-plugin-storybook”がインストールされます。
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の整合性を取る為のアドオンをインストールして設定しています。
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ファイルは以下のようになっています。
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) という拡張子で定義します。
(全体像)
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
export default {
title: "Button", // タイトル(任意)。 コンポーネント名と同じにするなら省略してもよさそう。
component: Button, // 実際に使用するコンポーネント(上でimportしたもの)
} as ComponentMeta<typeof Button>;
-
- Storybookで描画するためのコンポーネントの雛形を用意しておく
argsには、各ストーリーで後々設定するpropsが入ってきます。
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;
-
- bindを用いて雛形を元にしたコピーを作成
storiesファイルで名前付きエクスポートを行うと、デフォルトでストーリーオブジェクトであると認識されます。
export const Default: ComponentStory<typeof Button> = Template.bind({});
-
- 作成したコピーにそれぞれの引数を設定することで、様々なpropsに応じたコンポーネント(今回はボタン)を作り出すことができます。
以下はデフォルトのボタンを作り出したいので、カラーやサイズを渡していません。
- 作成したコピーにそれぞれの引数を設定することで、様々なpropsに応じたコンポーネント(今回はボタン)を作り出すことができます。
Default.args = { // 引数(props)を設定
label: "Default",
onClick: () => {
alert("クリック");
},
};
Default.storyName = "デフォルト";
.storyNameでそのストーリーのタイトルをつけることができます。
- サブボタンのストーリーも定義してみる
export const SubButton: ComponentStory<typeof Button> = Template.bind({});
SubButton.args = {
label: "SubButton",
backgroundColor: "#f6f0ea", // サブで作成したいボタンのカラーを設定
color: "#622d18",
};
SubButton.storyName = "サブボタン";
Storyの型定義について
型定義ではComponentMeta, ComponentStoryを使います。
import type { ComponentStory, ComponentMeta } from "@storybook/react";
export default {
title: "Button",
component: Button,
} as ComponentMeta<typeof Button>;
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をクリックすると、以下のような画面が出てきます。
左側に登録したコンポーネントの一覧が並び、それを選択することで、コンポーネントのUIを確認することができます。
下のControlsのところをいじると、その場でpropsを変更して、propsでの違いを見ることもできます。
- Docs
また、上のDocsメニューを押すと、以下のようなコンポーネントごとのドキュメントを見ることができます。
下の方へ行くと、定義したストーリーが並んでいます。
"Show code"で、その見た目にするにはどのようにpropsを渡せば良いかが一目瞭然なのでとても便利です!
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になることで...
- ストーリー定義がより簡潔に記述できるように👏
- intercative storyを記述できるように(play関数の登場)👏
ここでは一つ目、「ストーリー定義がより簡潔に記述できるように」という観点で、どのように変わったかを検証したいと思います。
上で、CSF2.0を用いて記述したボタンコンポーネントのストーリーを、CSF3.0に書き換えてみます。
Storybookのバージョンが6.4以上であれば、問題なくCSF3.0を利用することができます。
(私も2.0で書いていましたが、実は6.4以上だったのでCSF3.0で書ける状態でした。)
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など、各ストーリーに渡したいものを渡します。
// 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について