Storybook V5+React+TypeScriptで環境構築

![alt](https://storybook.js.org//images/logos/logo-storybook.svg)
[Storybook](https://storybook.js.org/)のV5.0がリリースされました。

Storybook+React+TypeScriptで試してみたので、内容をまとめてみました。
インストールからサンプルのコンポーネント作成と、GitHub Pagesへのデプロイまで記載しています。


まだドキュメントが古いものも多く、試行錯誤しながらでしたが、V4までと比べてTypeScript周りの設定が楽になった気がします。

なお、手順は`Node.js`や`create-react-app`はインストール済みの前提です。

[ソース](https://github.com/otanu/storybook-react-ts/tree/first-baseline)
[デモ](https://otanu.github.io/storybook-react-ts)

----
2019/03/09追記

-  `@storybook/addon-console`を追加
- `*.stories.js`もストーリの対象に追加
- `src/stories/*`の`js`と`jsx`はストーリ対象とするように修正
- withInfoを`addDecorator`で指定するように変更

# 土台の作成

まず、`create-react-app`でプロジェクトを作成します。

```sh
create-react-app storybook-react-ts --typescript
```

作成したら、Storybookを登録して初期化処理を実行します。

```sh
cd storybook-react-ts
yarn add -D @storybook/cli
yarn sb init
```

実行すると、Storybookの設定が入ったフォルダとストーリーのサンプルが追加されています。

```
.storybook/
  addons.js
  config.js
src/stories/
  index.js
```

また、`package.json`にStorybookの起動とビルドのコマンドが追加されます。

```json:package.json
  "scripts": {
    ~
    "storybook": "start-storybook -p 9009 -s public",
    "build-storybook": "build-storybook -s public"
  }
```

`yarn storybook`とコマンドを入れると、Storybookの画面が開きます。
<img width="882" alt="storybook-react-ts-01.png" src="https://qiita-image-store.s3.amazonaws.com/0/189588/b1b97ab8-23f3-3497-6826-71dc3e2e1036.png">

とりあえずこれで使えるようになりしたが、アドオンを追加したりストーリーをTypeScriptで作成できるようにしていきます。

# カスタマイズ
使いやすくするため、以下の作業を行っていきます。

- アドオン登録
  - addon-knobs
    プロパティを変更できる画面を追加
  - addon-viewport 
    ビューポートを切り替えるボタンを追加
  - addon-storysource
    ストーリーのソースを表示
  - addon-info
    ストーリーの例とか説明を追加
  - react-docgen-typescript-loader
    TypeScriptの型からコンポーネントのプロパティ説明を追加
  - addon-console
    Actionsタブにコンソールログを表示する
- ストーリーをTypeScriptで作成できるように
- `*.stories.tsx`のファイル名でストーリーを作成できるように

## アドオン追加
```sh
yarn add -D @storybook/addon-knobs \
  @storybook/addon-viewport \
  @storybook/addon-storysource \
  react-docgen-typescript-loader \
  @storybook/addon-info \
  @storybook/addon-console
```

## 型定義追加
```sh
yarn add -D @types/storybook__react \
  @types/storybook__addon-info \
  @types/storybook__addon-actions \
  @types/storybook__addon-knobs
```

## Storybookの設定変更
パッケージの登録が終わったら、Storybookの設定に定義を追加していきます。

```js:.storybook/addons.js
import '@storybook/addon-actions/register';
import '@storybook/addon-links/register';
import '@storybook/addon-knobs/register';
import '@storybook/addon-viewport/register';
import '@storybook/addon-storysource/register';
import '@storybook/addon-console';
```

次に、`.storybook/webpack.config.js`のファイルを作成し、
TypeScriptのソースをアドインに渡す処理を追加します。

```js:.storybook/webpack.config.js
const path = require("path");
module.exports = ({ config }) => {
  config.module.rules.push({
    test: /\.(ts|tsx)$/,
    use: [
      {
        loader: require.resolve("react-docgen-typescript-loader")
      },
      {
        loader: require.resolve('@storybook/addon-storysource/loader'),
        options: { parser: 'typescript' }
      }
    ]
  });
  config.resolve.extensions.push(".ts", ".tsx");
  return config;
};
```

次は、コンソールログをActionsタブに表示する処理と、TypeScriptで作成したストーリーを読み込む処理を追加します。

```js:.storybook/config.js
import { configure } from '@storybook/react';
import { setConsoleOptions } from '@storybook/addon-console';

setConsoleOptions({
  panelExclude: []
});

function loadStories() {
  let req = require.context("../src/stories", true, /.(tsx|js)$/);
  req.keys().forEach(filename => req(filename));

  req = require.context("../src", true, /.stories.(tsx|js)$/);
  req.keys().forEach(filename => req(filename));
}

configure(loadStories, module);
```

## コンポーネント作成

サンプルのコンポーネントを作成します。

```tsx:src/component/Example/index.tsx
import React, { useState, useCallback } from "react";

export interface ExampleProps {
  /** 表示するテキスト */
  text: string;
  /**
   * true: テキスト表示 false: テキスト非表示
   * @default false
   */
  flag?: boolean;
  /** ボタンを押した時のイベントハンドラ */
  action(): void;
}

const Example = (props: ExampleProps) => {
  const { text, flag, action } = props;
  const [count, countChg] = useState(0);
  const countUp = useCallback(() => countChg(prev => prev + 1), []);
  const countDown = useCallback(() => countChg(prev => prev - 1), []);

  return (
    <div>
      {flag && <p>{text}</p>}
      <button onClick={action}>ボタン</button>
      <p>count:{count}</p>
      <button onClick={countUp}>+</button>
      <button onClick={countDown}>-</button>
    </div>
  );
};

export default Example;
```

`addon-knobs`等のデモ用に、適当なプロパティを作っています。
また、Reactの最新版に対応しているかを確認するためReact Hooksを使ってます。
プロパティの定義にはコメントを入れておけばストーリに表示されるので、適当に入れておきます。

## ストーリー(TypeScript)追加

作成したコンポーネントに対応するストーリーを、登録したアドオンも使用して作成します。

```tsx:src/components/Example/index.stories.tsx
import React from "react";

import { storiesOf } from "@storybook/react";
import { withInfo } from "@storybook/addon-info";
import { withKnobs, text, boolean } from "@storybook/addon-knobs";
import { action } from "@storybook/addon-actions";

import Example from "../Example";

const components = storiesOf("Components", module);
components
  .addDecorator(withKnobs)
  .addDecorator(withInfo({ inline: true }))
  .add("Example", () => (
    <Example
      text={text("テキスト", "ああああ")}
      flag={boolean("テキスト表示", true)}
      action={action("ぽちっとな")}
    />
  ));
```

`yarn storybook`とコマンドを入れると、Storybookの画面が開きます。

<img width="1210" alt="storybook-react-ts-02.png" src="https://qiita-image-store.s3.amazonaws.com/0/189588/dd9b2b88-e120-c566-de2e-68296ebbd692.png">

# GitHub Pagesに登録

デプロイ用のツールを登録します。

```sh
yarn add -D gh-pages
```

登録したら、起動コマンドを追加します。

```json:package.json
  "scripts": {
    ~
    "predeploy": "build-storybook",
    "deploy": "gh-pages -d storybook-static"
  }
```
`predeploy`はデプロイ前の自動ビルドの設定、`deploy`では`build-storybook`で出力されるフォルダ`storybook-static`を指定します。

デプロイ実行は`yarn deploy`です。
完了すると以下のアドレスにデプロイされます。

`https://XXXXX.github.io/storybook-react-ts`

デプロイ時に作成されたファイルは不要だと思うので、`.gitignore`に登録しておきます。

```text:.gitignore
/storybook-static
```