React Native + Expo プロジェクトへのStorybook v6導入検討

<h2>はじめに</h2>

前回の記事(<a　href="https://qiita.com/suzuki0430/items/c5d8e066274e1178d19d">Create React AppのTypeScript対応プロジェクトにStorybook v6を導入した</a>)に引き続き、今回はReact Native + Expo のプロジェクトにStorybook v6を導入できるか検討してみました。
<h2>インストール</h2>

プロジェクト作成の前にNode.jsとyarnがインストールされていることを確認します。
以下のコマンドでプロジェクトを作成することができます。

```
expo init my-project
```

コマンドを実行するとtempleteを選ぶことができるので、今回はblank (TypeScript)を選択します。

```
$ expo init my-project
? Choose a template: › - Use arrow-keys. Return to submit.
    ----- Managed workflow -----
    blank                 a minimal app as clean as an empty canvas
❯   blank (TypeScript)    same as blank but with TypeScript configuration
    tabs (TypeScript)     several example screens and tabs using react-navigation and TypeScript
    ----- Bare workflow -----
    minimal               bare and minimal, just the essentials to get you started
    minimal (TypeScript)  same as minimal but with TypeScript configuration
```


<code>cd my-project</code>でフォルダに移動し、以下のコマンドでStorybookを導入することができます。

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

＊Create React Appでは<code>npx sb init</code>コマンドでv6が導入されましたが、expoではv5が導入されてしまうので注意が必要です。

以下のコマンドを実行することでStorybookが起動します。

```
yarn storybook
```

![スクリーンショット 2020-12-05 17.28.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/ea2a61fd-1428-0898-22f8-b0602c959187.png)


<h2>Storybookの関連ファイルとフォルダ</h2>

ここまでのコマンド実行でプロジェクト直下に.storybookとstoriesというフォルダが作成されていると思います。
＊Create React Appだとsrc/storiesというディレクトリ構成ですが、Expoの場合だとプロジェクト直下にstoriesフォルダが作成されます。

![スクリーンショット 2021-01-15 22.00.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/cd2ae070-fc10-4034-c154-8178afe1514a.png)

.storybookフォルダ内のファイル構成はv6から、main.jsとpreview.jsの2ファイルになりました。
main.jsではstoryのパスや適用するアドオンを定義し、preview.jsでは全ストーリーに対してグローバルにアドオンを適用したりすることができます。

main.js

```javascript
module.exports = {
  "stories": [
    "../stories/**/*.stories.mdx",
    "../stories/**/*.stories.@(js|jsx|ts|tsx)"
  ],
  "addons": [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-knobs"
  ]
}
```

preview.js

```javascript
export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
}
```

<h2>Storyの作成</h2>

今回はButtonコンポーネントを使って簡単なStoryを作成します。
stories直下にcomponentsフォルダを作成し、componentsフォルダの中でさらにコンポーネント単位でフォルダを作成します。
例えば、今回のケースでは、<code>stories/components/ButtonNative</code>の中に<code>Button.tsx</code>, <code>Button.stories.tsx</code>の2つファイルを作成します。

Button.tsx

```tsx
import React from 'react';
import { Button, ScrollView, View } from 'react-native';

const ButtonNative = (props) => {
  const { color, title, age, ...rest } = props;
  return (
    <ScrollView style={{ height: 1000 }}>
      <View style={{ width: 300 }}>
        <Button
          buttonStyle={{ width: 100 }}
          color={color}
          title={title}
          {...rest}
        />
      </View>
    </ScrollView>
  );
};

export default ButtonNative;
```
当たり前ですが、上記のようにScrollViewなどのReact Naviveコンポーネントにも対応しています。

Button.stories.tsx

```tsx
import React from 'react';
import ButtonNative from './Button';

export default {
  title: 'native/ButtonNative',
  compoent: ButtonNative,
};

const Template = (args) => <ButtonNative {...args} />;

export const PrimaryA = Template.bind({});
PrimaryA.args = {
  color: 'blue',
  title: 'Primary Args',
};

export const LongPrimaryA = Template.bind({});
LongPrimaryA.args = {
  ...PrimaryA.args,
  title: 'Long Primary Args',
};

export const SecondaryA = Template.bind({});
SecondaryA.args = {
  color: 'red',
  title: 'Secondary Args',
};
```

数箇所にわけて説明します。

```tsx
export default {
  title: 'native/ButtonNative',
  compoent: ButtonNative,
};
```
titleはStorybook内のサイドバーでの表示名を表します。
![スクリーンショット 2021-01-15 22.45.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/abe1d595-a5a3-7b09-741d-ca8cec7eff82.png)

componentにはimportしたtsxファイル名を指定します。


```tsx
const Template = (args) => <ButtonNative {...args} />;
```
こちらで各ストーリーのテンプレートを作成します。作成していくストーリーには、全てこのテンプレートを使用します。


```tsx
export const PrimaryA = Template.bind({});
PrimaryA.args = {
  color: 'blue',
  title: 'Primary Args',
};
```
PrimaryAというストーリーを作成しました。
作成したTempleteをbindし、colorとtitleにそれぞれ引数を与えます。
すると、Storybook内に作成したストーリーのコンポーネントが表示されます。
![スクリーンショット 2021-01-15 22.47.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/2770d2a5-bf7f-a7cb-e780-5e815d472e5c.png)



Button.stories.tsxにさらに2つのストーリーを追加します。

PrimaryAのcolorはそのままでtitleのみを変更したり、

```tsx
export const LongPrimaryA = Template.bind({});
LongPrimaryA.args = {
  ...PrimaryA.args,
  title: 'Long Primary Args',
};
```
![スクリーンショット 2021-01-15 22.53.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/a7e18a22-5ada-f2e0-766b-46644d1df9da.png)

colorとtitleを両方とも変更したりなど、自由にstoryを作ることができます。

```tsx
export const SecondaryA = Template.bind({});
SecondaryA.args = {
  color: 'red',
  title: 'Secondary Args',
};

```
![スクリーンショット 2021-01-15 22.54.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/b62c1fd9-b774-47db-952d-8c583b19707a.png)



<h2>アドオンの導入</h2>
 
Storybookには便利なアドオン(拡張機能)がいろいろ揃っています。
今回はaddon-knobsというアドオンを導入します。

アドオンのインストールには以下のコマンドを用い、インストール後はmain.jsに使用するアドオンを追記してください。


```
yarn add -D ~
```

main.js

```java
module.exports = {
  stories: [
    '../stories/**/*.stories.mdx',
    '../stories/**/*.stories.@(js|jsx|ts|tsx)',
  ],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-knobs',
  ],
};
```

<h3>addon-knobs</h3>

こちらのアドオンでは、Storybook下部のknobsタブ内でpropsを手動で変更することができます。
以下では、Buttonのtitle, color, disabledを変更できるようにしました。
＊addon-knobsはv6の書き方に対応していないようでしたので、v5の書き方で記載しました。

Button.stories.tsx

```tsx
export const Knobs = () => (
  <ButtonNative
    title={text('Name', 'ボタン')}
    color={color('Color', 'red')}
    disabled={boolean('Disabled', false)}
  />
);
```

以下のようにカラーマップから自由に色を変更したり、チェックボックスからDisabled状態に切り替えることができます。
![スクリーンショット 2021-01-15 23.04.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/1942ed73-a8e2-ead4-a0d9-d22be0093a79.png)

![スクリーンショット 2021-01-15 23.05.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/4d9c5551-2680-8bff-1454-bc7cd5d9e85a.png)


<h2>React Nativeプロジェクトにおけるアドオン</h2>
Storybookには、addon-knobsの他にも、addon-viewport, addon-console, addon-a11y, addon-docsなどの便利なアドオンが揃っています(詳しくは以下の記事をご覧ください)。
<a href="https://qiita.com/suzuki0430/items/c5d8e066274e1178d19d">Create React AppのTypeScript対応プロジェクトにStorybook v6を導入した</a>

しかし、React Nativeでは、なんとこれらのアドオンのほとんどには対応していません...
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/4c3986ec-d3f4-fc3b-fdde-74836b8920b3.png)

せめて市販端末の画面サイズでコンポーネント表示を確認できる、addon-viewportには対応してほしかったのですが...

(以下はCreate React Appで導入した際の画面です)

![スクリーンショット 2020-12-05 21.51.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/6779500f-f99b-0c14-57e5-f13091fde19b.png)

<h2>おわりに</h2>

ほとんどのアドオンに対応していないことから、現状では、実際のReact Native + ExpoプロジェクトにStorybookを導入するのは難しそうですね...

今後の公式の対応に期待したいと思います。