【Storybook × Next.js】Next.jsで始めるStorybookの始め方
Storybook × Next.js
Storybook
概要
ページとコンポーネントを分離して表示することのできるツール。
React(とりわけNextJS)においては、コンポーネントを作成してもすぐに確認できるわけではなく、一度ページを作成し、そこにコンポーネントを設置する必要があるが、Storybookはコンポーネントを作成しすぐに挙動や見た目を確認することができる
以下KOMODO DIGITAL社によるStorybook導入についてのページ、「React Storybook: Why Should You Use It? > Our Experience」より抜粋・翻訳
私たちの経験
Storybookを使い始めてから、他のプロジェクトの邪魔をすることなく、孤立した状態でコンポーネントをテストすることがいかに便利であるかに気づきました。
コンポーネントの典型的な状態を最もよく反映させるために、これらのストーリーでは文字通り何でもできます。個人的にStorybookの最も一般的な使い方は、開発中に一般的な遊び場として使うことと、プルリクエストの一部として提出されるビデオの形でコンポーネントを紹介することの2つです。
しかし、React Storybookは、コンポーネントを簡単かつ効果的にテストできるため、ミーティング中にクライアントと使用したり、QAチームに渡すための素晴らしいデモンストレーションツールになります。
利点
筆者が考える利点(Storybookを使うとで改善できた点)として三点
コンポーネントの疎結合化の意識がしやすくなる
個人的に最も大きい利点に感じた
Reactの真髄はコンポーネントが独立し、使いまわせるほどに汎用的に機能を作成できる点だが、筆者の場合開発していると密結合になり得るコンポーネントを作ってしまうことがある。
機能として全く異なるもののはずが、一方のコンポーネントが欠けるともう一方が機能しなくなってしまうということ
Storybookは開発者に常にこの気づきを与えてくれている。後ほど詳細に記載するが、Storybookは各コンポーネントをディレクトリのように表示するため二つの密結合になっているコンポーネントが存在すると開発者が違和感に気がつくことができる
Indexにとりあえず配置して動作確認なんてしない
Storybookはコンポーネント単位で表示できることに加えPropsの値などもStorybook上の画面で自在にスイッチできる。
筆者の場合、作りはじめはとりあえずindexファイルにコンポーネントを放り込んで挙動を確認し、ある程度コンポーネントが揃ってからレイアウト・ページを作成していたが、この開発方法はなんとなく突貫工事感があるというか、ちゃんとした人はやっていないんだろうな。と思いながら開発していたため、やはりStorybookを触った時は感動した。
そのほかにもとりあえずコンポーネントを設置するという行為は、必然的にPropsもとりあえず設定するということになる。表示するかしないか(あるいは広げるか狭めるか等)の判断をするPropsにindex上でtrue/falseを指定、そこのソースを直接書き換えて保存、挙動を確認...という確認方法を一度は経験しているはず
const Home: NextPage = () => {
return <AwesomeComponent isOpen={true} />
}
レビューがしやすい
コンポーネント単位で表示できて、PropsもStorybook上で操作できるということは、レビューを依頼する際や成果物を第三者が確認するときに有効。
上記二点でも紹介した通りPropsの指定が必要だったり、結合しすぎたコンポーネントはレビューの際に再現が結構面倒だったりするケースがよくあり実際のレビューに入るまでに開発者とレビュー者の時間を多く消耗する結果を招くため、コンポーネント単位で動作するように実装できるStorybookはやはり偉大
インストール
Storybookのインストールと初期化
- プロジェクトのルートディレクトリで下記コマンドを実行
npx storybook init
- ルートディレクトリに.storybookというディレクトリができていれば完了
Storybookを起動
npm
npm run storybook
yarn
yarn storybook
デフォルトの設定では以下でアクセス可能
StorybookのIntroductionが表示されていれば成功
使用方法
前提
環境
- Next JS
- React by Typescript
参考
画面の操作
下図がStorybookのデフォルトの画面(見やすくするためにズーム率150%で表示)。
Storybookではinitした時点でイグザンプルズコンポーネントが用意されているため、今回はそれを使用して解説
四角内の数字をここではNo.1〜No.4と表現する
No.1 サイドバー
作成したストーリーを格納するエリア。ここでコンポーネントを選択するとNo.3のキャンバスにコンポーネントが表示される。
また画像ではButton/Primaryが選択されているが、親要素として「コンポーネント」を。子要素として「コンポーネントの状態」を配置する構成が公式の推奨らしい
なお、親要素(ここではButton)を選択するとデフォルトで一番上のPrimaryが選択される。
No.2 タブ
Canvas(コンポーネントの描画エリア)とDocs(コンポーネントの情報エリア)の切り替えが可能
No.3 キャンバス
CanvasとDocsの2種類があり、Canvasでは実際のコンポーネントの見た目や挙動を確認できる。
Docsではコンポーネント自体の説明や再現方法、Propsの詳細などを確認できる。
下図はDocsタブを開いた時の画面
No.4 コントロールパネル
No.3のタブでキャンバスを選ぶと表示されるエリアで、Propsの情報を自由に操作できる。
ただの文字列でPropsを指定することもできるが
backgroundColorのコントロール方法をカラーピッカーに変更したり、リテラル型を用いることで指定したいくつかの文字列をラジオボタンに表示することもできる
今回の例ではButtonコンポーネントのPropsにあるsizeがそれにあたる
実装
Storybookを設定する
インストールの手順が完了していればルートディレクトリに「.srotybook」というディレクトリが作られているので、「.storyies/main.js」を確認
stories配列の中身が以下になっていることを確認し設定は終了
"stories": [
...
"../src/**/*.stories.@(js|jsx|ts|tsx)"<= ここでStorybookファイルの置き場を指定する
],
上記設定が完了するとsrc配下にあるXXXX.stories.tsxファイルをstorybookが解釈できるようになる。
※下記の状態であればOK
module.exports = {
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions"
],
"framework": "@storybook/react",
"core": {
"builder": "@storybook/builder-webpack5"
}
}
Reactコンポーネントを実装する
ここは皆さんご存知だと思うので説明は割愛。以下の内容で実装(といってもStorybookのイグザンプルズをそのまま掲載)
※Reactの実装は知っているのでStorybookの実装方法だけ知りたいという方はこちらへ
import React from 'react';
import './button.css';
interface ButtonProps {
primary?: boolean;
backgroundColor?: string;
size?: 'small' | 'medium' | 'large';
label: string;
onClick?: () => void;
}
export const Button = ({
primary = false,
size = 'medium',
backgroundColor,
label,
...props
}: ButtonProps) => {
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
return (
<button
type="button"
className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
style={{ backgroundColor }}
{...props}
>
{label}
</button>
);
};
CSSを当てる
.storybook-button {
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-weight: 700;
border: 0;
border-radius: 3em;
cursor: pointer;
display: inline-block;
line-height: 1;
}
.storybook-button--primary {
color: white;
background-color: #1ea7fd;
}
.storybook-button--secondary {
color: #333;
background-color: transparent;
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
}
.storybook-button--small {
font-size: 12px;
padding: 10px 16px;
}
.storybook-button--medium {
font-size: 14px;
padding: 11px 20px;
}
.storybook-button--large {
font-size: 16px;
padding: 12px 24px;
}
ここまででReactコンポーネントは完成
index.tsxやpagesに作成したコンポーネントを設置すれば以下のコンポーネントが表示される
Storybookへコンポーネントを読み込ませる
ここからいよいよStorybookの設定に入ります
ファイル作成
コンポーネントファイルと同名で.stories.tsxファイルを作成する
今回の場合「Button.stories.tsx」となる
今回のディレクトリ構成はこんな感じ
|
|
*-component
| |
| *- Button.tsx
| *- button.css
| *- Button.stories.tsx
|
*- .........
モジュールインポート
StorybookからComponentStory, ComponentMetaと作成したコンポーネントをインポートする
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { Button } from './Button';
- ComponentStory:コンポーネントの状態を変えるためのテンプレートの型
- ComponentMeta:実際にStorbookのCanvasに表示するための型
コンポーネントの出力方法を設定する
ComponentMetaの型でオブジェクトをエクスポートする
export default {
title: 'Example/Button',
component: Button,
argTypes: {
backgroundColor: { control: 'color' },
},
} as ComponentMeta<typeof Button>;
- title:階層表現で指定すると、画面図のNo.1にあるサイドバーのように親子の関係で表示される
- component:インポートしたコンポーネントを指定
- argTypes:Storybook上でのPropsの選択方法を指定する
-> Propsをキーとしたオブジェクト型で指定する
※以下代表的なものを抜粋
カラーピッカー
// backgroundColorというPropsを持つコンポーネントの場合
argTypes: {
backgroundColor: { control: 'color' },
},
セレクトボックス
// loginStatusというPropsを持つコンポーネントの場合
argTypes: {
loginStatus: { control: 'select', options:["login", "logout"] },
},
また、argTypesに型を指定しなくてもコンポーネントの型として指定している場合は自動的にそれに合った選択方法となる
※例えばリテラル型のPropsであればラジオボタンになるし、boolean型ならスイッチボタンになる
Templateを作成する
Templateを作成することで、コントロールパネルの値をいちいち変えなくてもスライドバー(No.1)のButton配下に出てきてくれるので即座の切り替えが可能
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;
Templateにargsを指定し、Storybook上に出力する
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',
};
実装のまとめ
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { Button } from './Button';
export default {
title: 'Example/Button',
component: Button,
argTypes: {
backgroundColor: { control: 'select', options:["black", "white"] },
},
} as ComponentMeta<typeof Button>;
const Template: ComponentStory<typeof Button> = (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',
};
Discussion