Storybook v5 を TypeScript × React 環境に導入する

July 29, 2019

以前、GatsbyJS × TypeScript製のサイトにStorybookを導入する ではGatsbyJSの環境でセットアップしました。今回は通常のTypeScript × React環境にStorybookをセットアップしたいと思います。

React環境でStorybookをインストールする

Storybook for ReactAutomatic setupに沿ってやっていきます。手動で入れたい場合はManual setupを参照してください。

npx -p @storybook/cli sb init --type react

以下のライブラリが自動でインストールされます。

"@babel/core": "^7.5.5",
"@storybook/addon-actions": "^5.1.9",
"@storybook/addon-links": "^5.1.9",
"@storybook/addons": "^5.1.9",
"@storybook/react": "^5.1.9",
"@types/storybook__react": "^4.0.2",
"babel-loader": "^8.0.6",

以下のnpm scriptも自動で挿入されます。

"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"

以下の設定ファイルも自動で作成されます。

.storybook/addmins.js
.storybook/config.js

.storybook/addons.jsは以下の通りです。

import '@storybook/addon-actions/register';
import '@storybook/addon-links/register';

.storybook/config.jsは以下の通りです。

import { configure } from '@storybook/react';

// automatically import all files ending in *.stories.js
const req = require.context('../stories', true, /\.stories\.js$/);
function loadStories() {
  req.keys().forEach(filename => req(filename));
}

configure(loadStories, module);

stories/index.stories.jsも自動生成されます。

import React from 'react';

import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { linkTo } from '@storybook/addon-links';

import { Button, Welcome } from '@storybook/react/demo';

storiesOf('Welcome', module).add('to Storybook', () => <Welcome showApp={linkTo('Button')} />);

storiesOf('Button', module)
  .add('with text', () => <Button onClick={action('clicked')}>Hello Button</Button>)
  .add('with some emoji', () => (
    <Button onClick={action('clicked')}>
      <span role="img" aria-label="so cool">
        😀 😎 👍 💯
      </span>
    </Button>
  ));

これでReact環境にStorybookをセットアップできました。昔に比べると導入がめちゃくちゃ簡単になっています。

npm run storybook

起動できました。

          2019 07 29 22 23 10

TypeScriptに対応させる

こちらの記事を参考に進めました。babel-loaderを使う方法が紹介されていますが、今回はts-loaderを使用しました。

Setting up TypeScript with babel-loader

インストール

型定義とts-loaderをインストールします。babel-loaderもインストールする必要があります。

npm i -D @types/storybook__react ts-loader babel-loader

TypeScriptを読み込めるようにする

Storybookでは.storybook/webpack.config.jsを自前で準備することができます。そこにTypeScriptを読み込む設定を加えます。

Custom Webpack Config

.storybook/webpack.config.jsはこのようにしました。ts-loaderを使用しています。

module.exports = {
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: "ts-loader"
      }
    ]
  },
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".json"]
  }
};

.storybook/config.js.jsではなく.tsxファイルを読み込むように書き換えます。

...
const req = require.context("../src", true, /\.stories\.tsx$/);
...

react-router-domを使用している場合のstoriesの書き方

react-router-domを使用している場合のコンポーネントのPropsはRouteComponentPropsという型になります。

また<Link />コンポーネントを使用している場合は<Router /><Route />を使う必要があります。

これらをstoriesファイル内でエミュレートしてやる必要があります。

以下のissueに対応策がありました。

Component with react router · Issue #769 · storybookjs/storybook · GitHub

よくあるナビゲーションメニューのコンポーネントNavigation.tsx。react-router-domの<Link />を使用しています。これをそのままstoriesに書いてしまうとエラーが出てしまいます。

import React from "react";
import { Link, RouteComponentProps } from "react-router-dom";

const LINKS = [
  { to: "/", label: "Home" },
  { to: "/about", label: "About" },
  { to: "/profile", label: "Profile" }
];

const Navigation = (props: RouteComponentProps) => {
  return (
    <nav>
      <ul>
        {LINKS.map(l => {
          return (
            <li key={l.to}>
              <Link to={l.to}>
                {l.label}
              </Link>
            </li>
          );
        })}
      </ul>
    </nav>
  );
};

export default Navigation;

<Router />addDecoratiorで使うことでエミュレートできます。

Navigation.stories.tsxはこんな感じです。RouteComponentPropsであるhistorylocationmatchもエミュレートしてやる必要があります。

import React from "react";
import { storiesOf } from "@storybook/react";
import Navigation from "./Navigation";
import { Router, Route, match } from "react-router";
import { createMemoryHistory, createLocation } from "history";

const match: match<{}> = {
  isExact: false,
  path: "/",
  url: "/",
  params: {}
};

storiesOf("Navigation", module)
  .addDecorator((story: () => JSX.Element) => (
    <Router history={createMemoryHistory()}>
      <Route path="*" component={() => story()} />
    </Router>
  ))
  .add("with text", () => {
    return (
      <Navigation
        history={createMemoryHistory()}
        location={createLocation("")}
        match={match}
      />
    );
  });

これでひとまず起動することができました。