React Native(Expo) + NativeBase環境にStorybook v6(CSF3.0)を導入

2022/03/02に公開

課題

StorybookをReact Native環境に導入する際、公式のチュートリアルで紹介されている手順通りに進めると古いバージョンがインストールされてしまいます。

https://storybook.js.org/tutorials/intro-to-storybook/react-native/en/get-started/

2022年3月現在、最新のStorybookは6.4系ですが、以下コマンドを実行することで導入されるStorybookは5.3系です。

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

(補足:公式の@storybook/react-nativeリポジトリは若干動きがあり、2022年4月時点でv6.0.1-beta.5リリースされています。こちらの進捗次第で、本記事の内容は古くなるor意味がなくなる可能性があります)

+    "@storybook/addon-actions": "^5.3",
+    "@storybook/addon-knobs": "^5.3",
+    "@storybook/addon-links": "^5.3",
+    "@storybook/addon-ondevice-actions": "^5.3.23",
+    "@storybook/addon-ondevice-knobs": "^5.3.25",
+    "@storybook/react-native": "^5.3.25",
+    "@storybook/react-native-server": "^5.3.23",

Storybookはversion 6.3以降でComponent Story Format 3.0、通称CSF3.0を活用してこれまでと比較してシンプルなストーリーを記述できます。

https://storybook.js.org/blog/component-story-format-3-0/

React NativeであってもCSF3.0の恩恵を受けたいと思い、本記事の手順の通りセットアップしてみたので記事に残します。

本記事の環境情報

  • Expo 42系
  • NativeBase 3.2系
  • Storybookおよび関連パッケージ 6.4系

免責事項

  • 筆者は生まれて初めてStorybookを触ってみたガチ初心者なので、的外れなことを説明しているかもしれません
  • 導入しただけなので、実際にコンポーネント開発に適用し始めるとどうなるかわかっていません
  • 結論を言えば、 react-native-webを使うことでReact Native対応したことにしています
  • 執筆時点のREADME.mdではReact Nativeのv6.4系のDemoが記載されていないなど、公式のサポート状況に疑問が残りますので、その点留意の上で導入などご判断ください

導入手順

reactをターゲットに指定してsb initする

react_nativeではなく、reactをターゲットにしてinitします。

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

これは相当時間が掛かるので、お茶でも飲みながら待ちます。

main.jsの書き換え

自動生成された.storybook/main.jsを書き換えます。

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',
  // https://zenn.dev/himorishige/scraps/8cca98dc9120d2
  webpackFinal: (config) => {
    config.resolve.alias = {
      'react-native$': 'react-native-web',
    };
    // https://github.com/react-native-svg/react-native-svg/issues/1553#issuecomment-1011487502
    config.resolve.extensions.unshift('.web.js');
    return config;
  },
};

webpackFinalの項目がポイントです。

https://storybook.js.org/docs/react/configure/overview#configure-your-storybook-project

react-native-webを使う

コメントに記載しているScrapを参考に、react-nativeを使っている箇所を全てreact-native-webとしてWebpackが解決するようにAlias設定をします。

https://webpack.js.org/configuration/resolve/

こういう立ち振る舞いができるのはReact Nativeの本領発揮というところですね。
※念のためですが、このアプローチを取っている時点で、ネイティブ独自の挙動をStorybook上で確認することは不可能になるはずなので、ご了承ください

react-native-svgのエラーを解消する

続いて、config.resolve.extensions.unshift('.web.js');の部分についてです。
こちらの設定をしないと、以下のようなエラーが出ました。もしかしたらNativeBaseのコンポーネントを使ったStoryを含まない状態ではエラーが出ないかもしれません。

ModuleParseError: Module parse failed: Unexpected token (18:12)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| const AssetSourceResolver = require('./AssetSourceResolver');
|
> import type {ResolvedAssetSource} from './AssetSourceResolver';
|
| let _customSourceTransformer, _serverURL, _scriptURL;

この件についてググりまくっていたところ、上記の修正案を見つけたので対応できました。ざっくりいうと、react-native-webに変換することで、web.jsという拡張子に一部のコンポーネントが変換されるようなので、それをWebpackの解決対象に入れることが必要、ということみたいですね。

https://github.com/react-native-svg/react-native-svg/issues/1553#issuecomment-804089936

https://webpack.js.org/configuration/resolve/#resolveextensions

NativeBaseProviderを設定する

NativeBaseのコンポーネントを含んでいる場合は、Providerでラップしてあげる必要があるので、.storybook/preview.jsを以下のようにします。

import { NativeBaseProvider } from 'native-base';
import { theme } from '~/utils/themes/NativeBase';
import React from 'react';

export const parameters = {
  actions: { argTypesRegex: '^on[A-Z].*' },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
};

const withThemeProvider = (Story, context) => {
  return (
    <NativeBaseProvider theme={theme}>
      <Story {...context} />
    </NativeBaseProvider>
  );
};
export const decorators = [withThemeProvider];

僕の環境ではthemeだけを持ってくればOKでしたが、その他必要に応じて引数をセットアップしてください。

React Native Web addon for Storybookインストール

上記の設定時点でも簡単なコンポーネントであればStorybook上で確認できたのですが、Iconを使っているコンポーネントで失敗しました。

そこで、本記事の方法でreact-native-webを使っていることを活用し、React Native Web addon for Storybookを追加で設定します。

yarn add babel-plugin-react-native-web @storybook/addon-react-native-web --dev
    // see https://github.com/storybookjs/addon-react-native-web README
    {
      name: '@storybook/addon-react-native-web',
      options: {
        modulesToTranspile: ['react-native-vector-icons'],
        babelPlugins: ['@babel/plugin-transform-react-jsx'],
      },
    },

babelPluginsの設定はExpo42だったので必要なのかもしれません。みなさんの環境でもどの程度必要なのかは検証してみてください。


ここまでの設定をしたのち、yarn storybookを実行すると以下のようにNativeBaseのButtonコンポーネントを使ったコンポーネントをStoryとして開くことができました。

補足

V7に備えた設定について

公式ドキュメントを見る限り、次のメジャーアップデートであるV7に備えてすでにFeature Flagが出ているみたいなので、これから導入する方は早めにチャレンジしてもいいかなと思いました。

https://storybook.js.org/docs/react/configure/overview#feature-flags

Babelについて

Babelの設定をカスタムする必要があるのかなと思っていたのですが、そうでもなかったです。

CSF3.0について

以下のように型安全かつ超シンプルにStoryが書けます。便利ですね。タイトルはデフォルトでディレクトリ構造を反映したものになっているようです。

import { ComponentMeta, ComponentStoryObj } from '@storybook/react';
import ActionButton from '~/components/ui/button/ActionButton';
import Colors from '~/utils/themes/Colors';

export default {
  component: ActionButton,
} as ComponentMeta<typeof ActionButton>;

export const Primary: ComponentStoryObj<typeof ActionButton> = {
  args: {
    size: 'md',
    children: '送信する',
  },
};

まとめ

Storybookはいつか運用してみたいなと思って定期的にウォッチしていたのですが、特にCSF3.0の書きかたが便利になっていたり、昨今はmswやtesting-libraryとのインテグレーションもできることから、フロントエンド開発において当たり前のように開発フローに含むことのできるツールに進化しているのかな?と初心者としては思っているところです。

もし運用面などについてお話してくれる方がいらっしゃればTwitterなどでご連絡ください!

参考文献

Discussion