React + TypeScript: Storybookを使ってみる

[Storybook](https://storybook.js.org/)は、ユーザーインタフェースやページの振る舞いを、コンポーネントごとに確かめ、試せるツールです。本稿は、ReactにTypeScriptを組み込んだプロジェクトで、Storybookの使い方について簡単にご説明します。

# プロジェクトをつくる
React + TypeScriptのひな形プロジェクトは、[Create React App](https://ja.reactjs.org/docs/create-a-new-react-app.html#create-react-app)のつぎのコマンドでつくりましょう(プロジェクト名は`react-typescript-storybook`としました)。TypeScriptを環境に加えるオプションが`--template typescript`です(「[Create React AppでTypeScriptを使用する](https://ja.reactjs.org/docs/static-type-checking.html#using-typescript-with-create-react-app)」)。

```terminal
npx create-react-app react-typescript-storybook --template typescript
```

つぎに、Storybookをインストールします(「[Install Storybook](https://storybook.js.org/docs/react/get-started/install)」)。つくられたプロジェクト(`react-typescript-storybook`ディレクトリ)のルートで、つぎのコマンドを打ち込んでください。

```terminal
npx storybook init
```

Storybookの初期画面は、`npm run storybook`で開きます(Reactプロジェクトを起動する必要はありません)。URLは`http://localhost:6006/`です。

サンプルのstoryモジュール(`stories.tsx`)やそれらの用いるコンポーネント(`tsx`)がつくられ、`src/stories`に収められています。実際には、storyモジュールと分けるため、Reactコンポーネントは`src`下の別フォルダに加えることになるでしょう。

![qiita_2302001_005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/631774cd-456b-cab3-c0ff-1f51fe31f744.png)

今回は、Reactコンポーネントとして`src/stories/Button.tsx`を使います(コードはつぎのとおり)。`src/stories/Button.css`がコンポーネントのスタイルを決めるCSSです。本稿では、story(`stories.tsx`)以外のモジュールについては、詳しく触れません。

```react:src/stories/Button.tsx
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>
	);
};
```


# storyを書いてみる
早速、storyを新たなモジュール`src/stories/HelloButton.stories.tsx`として書きましょう。

ディレクトリは`stories`内です。storyのコンポーネントは`default`で`export`してください(「[Default export](https://storybook.js.org/docs/react/api/csf#default-export)」)。型は`ComponentMeta`で定めます。プロパティの`title`はStorybookの画面左カラムに示されるstory名です。表示するReactコンポーネントを`component`に与えます。

名前つきで`export`する関数(`HelloButton`)が具体的なstoryの項目(オブジェクト)です(「[Named story exports](https://storybook.js.org/docs/react/api/csf#named-story-exports)」)。戻り値のJSXがReactコンポーネントの表示を定めます(「[Defining stories](https://storybook.js.org/docs/react/writing-stories/introduction#defining-stories)」参照)。

```react:src/stories/HelloButton.stories.tsx
import { ComponentMeta } from '@storybook/react';
import { Button } from './Button';

export default {
	title: 'Button',
	component: Button,
} as ComponentMeta<typeof Button>;
export const HelloButton = () => <Button label="Hello!!" />;
```

`Button`コンポーネントが受け取ったプロパティ`label`は、ボタンに示されるテキストです。

```react:src/stories/Button.tsx
export const Button = ({

	label,

}: ButtonProps) => {

	return (
		<button

		>
			{label}
		</button>
	);
};
```

Storybookの画面左カラムにstoryのタイトル「Button」が加わりました。展開すると、名前つき`export`したstoryオブジェクト(項目)の識別子(`HelloButton`)がスペース区切りで示されます。なお、storyオブジェクト名には、アッパーキャメルケース(パスカルケース)を使いましょう(「[キャメルケース【camel case】](https://e-words.jp/w/%E3%82%AD%E3%83%A3%E3%83%A1%E3%83%AB%E3%82%B1%E3%83%BC%E3%82%B9.html)」参照)。

> Use the **named** exports of a CSF file to define your component’s stories. We recommend you use UpperCamelCase for your story exports.
(「[Defining stories](https://storybook.js.org/docs/react/writing-stories/introduction#defining-stories)」)

story項目「Hello Button」を選ぶと、`Button`コンポーネントはデフォルトのスタイルで表示され、`label`のテキストがボタンに示されるでしょう。

![qiita_2302001_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/3a134582-13f1-2008-1ef7-536786664184.png)

ところで、あらかじめサンプルに加えられたstoryは「Example」という名前でグループ化されています。これは、`title`をグループ名ではじめ、storyコンポーネント名はそのあとにスラッシュ(`/`)区切りで添えたからです(「[Grouping](https://storybook.js.org/docs/react/writing-stories/naming-components-and-hierarchy#grouping)」参照。)

```react:src/stories/Button.stories.tsx
export default {
	title: 'Example/Button',
	component: Button,

} as ComponentMeta<typeof Button>;
```


# storyオブジェクト(項目)を追加する
名前つき`export`するstoryオブジェクト(項目)は、ひとつのstoryコンポーネント(モジュール)にいくつでも加えられます。`Button`コンポーネントにはオプションのプロパティがありますので、それを新たなstoryオブジェクト`PrimaryLargeButton`に定めましょう。プロパティ`primary`はカラーテーマを使う論理値、`size`が3段階の大きさです。


```react:src/stories/HelloButton.stories.tsx
import { ComponentMeta } from '@storybook/react';
import { Button } from './Button';

export default {
	title: 'Button',
	component: Button,
} as ComponentMeta<typeof Button>;
export const HelloButton = () => <Button label="Hello!!" />;
export const PrimaryLargeButton = () => <Button label="Primary Large" primary={true} size="large" />;
```

どちらのプロパティも、`Button`コンポーネントに与えるCSSのクラス(`className`)でスタイルを切り替えます。

```react:src/stories/Button.tsx
export const Button = ({
	primary = false,
	size = 'medium',

}: ButtonProps) => {
	const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
	return (
		<button

			className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}

		>

		</button>
	);
};
```

Storybookでストーリー項目の「Primary Large Button」を選べば、背景色は水色で白抜き文字の大きなボタンが表示されるでしょう。

![qiita_2302001_002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/40306734-2d66-5f17-40a4-89aac9215260.png)


# テンプレートを使う
storyオブジェクトのプロパティはテンプレートのJSXに直に書き込むのでなく、値も含めて分けられれば管理しやすく便利です。その場合、テンプレートの`args`にプロパティを定めます(「[Using args](https://storybook.js.org/docs/react/writing-stories/introduction#using-args)」参照)。

まず、テンプレートのコールバックが返すJSXから、具体的なプロパティや値の記述は除いてください。つぎに、各storyオブジェクトにはテンプレートの関数をコピーします。このとき用いるのが、[`Function.prototype.bind()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Function/bind)メソッドです。

> 💡 `Template.bind({})` is a [standard JavaScript technique](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Function/bind) for making a copy of a function. We copy the Template so each exported story can set its own properties on it.
(「[Using args](https://storybook.js.org/docs/react/writing-stories/introduction#using-args)」)

そのうえで、storyオブジェクトの`args`に、プロパティと値をオブジェクトで与えてください。このオブジェクトがテンプレートのコールバックに引数(`args`)として渡されます。戻り値のJSXには、引数のプロパティと値を加えればよいのです。

```react:src/stories/HelloButton.stories.tsx
// import { ComponentMeta } from '@storybook/react';
import { ComponentMeta, Story } from '@storybook/react';
import { Button } from './Button';

export default {
	title: 'Button',
	component: Button,
} as ComponentMeta<typeof Button>;
const Template: Story<typeof Button> = (args) => <Button label='' {...args} />;
// export const HelloButton = () => <Button label="Hello!!" />;
export const HelloButton = Template.bind({});
HelloButton.args = {
	label: 'Hello!!'
};
// export const PrimaryLargeButton = () => <Button label="Primary Large" size="large" primary={true} />;
export const PrimaryLargeButton = Template.bind({});
PrimaryLargeButton.args = {
	label: 'Primary Large',
	primary: true,
	size:'large',
}
```


# `control`を操作する
テンプレートに`args`を定めると、Storybook下部の[Controls]パネルが使えるようになります。各プロパティについて、`args`で与えた以外の値も試せるのです。値のリセットは、パネル右上の[Reset controls]ボタンをクリックしてください。

![qiita_2302001_003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/28de5d59-6253-a94b-e239-da3f5b7bbca5.png)

[Controls]パネルの操作オプションは、カスタマイズできます。そのとき用いるのが、`argTypes`です(「[ArgTypes](https://storybook.js.org/docs/react/api/argtypes)」参照)。

`Button`コンポーネントのプロパティ`backgroundColor`には初期値がありません。選択できるプロパティ値をラジオボタンで[Controls]パネルに加えてみましょう。

```react:src/stories/Button.tsx
export const Button = ({

	backgroundColor,

}: ButtonProps) => {

	return (
		<button

			style={{ backgroundColor }}

		>

		</button>
  );
};
```

`argTypes`はstoryコンポーネントに加えます。`backgroundColor`に定めるオブジェクトの`options`が値の配列で、`control`は操作方法(`{ type: 'radio' }`)です。

```react:src/stories/HelloButton.stories.tsx
import { ComponentMeta, Story } from '@storybook/react';
import { Button } from './Button';

export default {
	title: 'Button',
	component: Button,
	argTypes: {
		backgroundColor: {
			options: ['#1ea7fd', '#e57373', '#ffb74d', 'transparent'],
			control: { type: 'radio' },
		},
	},
} as ComponentMeta<typeof Button>;
const Template: Story<typeof Button> = (args) => <Button label='' {...args} />;
export const HelloButton = Template.bind({});
HelloButton.args = {
	label: 'Hello!!'
};
export const PrimaryLargeButton = Template.bind({});
PrimaryLargeButton.args = {
	label: 'Primary Large',
	size:'large',
	primary: true,
}
```

これで、ボタンの背景色(`backgroundColor`)は4つのラジオボタンから選べるようになりました。

![qiita_2302001_004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/e0693044-ef31-0994-bf52-5b39fa9ef87d.png)

なお、`control`を`{ type: 'select' }`にすれば、オプションをプルダウンメニューから選択できます。

```react:src/stories/HelloButton.stories.tsx
export default {

	argTypes: {
		backgroundColor: {

			// control: { type: 'radio' },
			control: { type: 'select' },
		},
	},
} as ComponentMeta<typeof Button>;
```

以上が、React + TypeScriptでStorybookをどのように使うかの基本的なご説明です。サンプルのプロジェクトにあらかじめ加えられたstoryについても、モジュールを開いておおむね理解いただけると思います。さらに詳しくは、[Storybookの公式ドキュメント](https://storybook.js.org/docs/react/get-started/introduction)をお読みください。