Slide 1

Slide 1 text

React Native アプリに Storybook CSF3.0 を導入しよう meijin

Slide 2

Slide 2 text

自己紹介

Slide 3

Slide 3 text

自己紹介 名人 Twitter: @meijin_garden 株式会社NoSchool CTO オンライン家庭教師サービスの「マナリンク」を 開発しています 普段はPM したり、Laravel でAPI 書いたり、 Nuxt でフロントエンド書いたり、React Native 書い てます ちょうど先週から今週に掛けてExpo42 →45 へのア プデ&EAS Build 移行をやって大量のバグを踏んで ましたw

Slide 4

Slide 4 text

マナリンクについて①

Slide 5

Slide 5 text

マナリンクについて② 一言で言うと オンライン家庭教師と生徒のマッチングプラットフォーム を提供することで 日本中・世界中の先生が活躍できる場所を作っています 開発しているプロダクトは オンライン家庭教師特化の検索サイト オンライン指導専用アプリ (React Native)

Slide 6

Slide 6 text

今回の発表内容

Slide 7

Slide 7 text

React Native アプリに Storybook CSF3.0 を導入しよう

Slide 8

Slide 8 text

説明の流れ Storybook とは ストーリーの書き方 React Native アプリへのStorybook 導入 Storybook を動かす msw(Mock Service Worker) の導入 Scaffdog でチーム運用に乗せる まとめ

Slide 9

Slide 9 text

前提 環境 React Native Expo (Managed Workflow ) Native Base Storybook を導入してみたタイミングのため、運用の知見はまだありません

Slide 10

Slide 10 text

Storybook とは

Slide 11

Slide 11 text

Storybook とは? コンポーネントを実際にページに埋め込むこと無く単体で開発・テスト・運用できるようにするツール Storybook があると嬉しいこと コンポーネント単位での動作確認が楽になる ページを実際に開く必要がなくなる バックエンドを起動しなくても確認できる コンポーネント内の条件分岐が可視化される 「ストーリー」 にコンポーネントの表示パターンを一覧できる 「このパターンの表示忘れていた!」といった改修漏れが防げる

Slide 12

Slide 12 text

ストーリーの例 Button.tsx のストーリーとして Button.stories.tsx の例 ` ` ` ` import { ComponentMeta, ComponentStoryObj } from '@storybook/react'; import Button from './index'; export default { component: Button, } as ComponentMeta; export const Default: ComponentStoryObj = { args: { children: ' 送信する', }, }; export const IsLoading: ComponentStoryObj = { args: { isLoading: true, children: ' 送信する', }, };

Slide 13

Slide 13 text

ストーリーの書き方 import { ComponentMeta, ComponentStoryObj } from '@storybook/react'; import Button from './index'; export default { component: Button, } as ComponentMeta; export const Default: ComponentStoryObj = { args: { children: ' 送信する', }, }; export const IsLoading: ComponentStoryObj = { args: { isLoading: true, children: ' 送信する', }, }; default export でメタ情報をエクスポートする ストーリーとして書きたいコンポーネント( 必須) や、 ストーリー自身のタイトル( 省略可) を指定

Slide 14

Slide 14 text

ストーリーの書き方② import { ComponentMeta, ComponentStoryObj } from '@storybook/react'; import Button from './index'; export default { component: Button, } as ComponentMeta; export const Default: ComponentStoryObj = { args: { children: ' 送信する', }, }; export const IsLoading: ComponentStoryObj = { args: { isLoading: true, children: ' 送信する', }, }; const export でコンポーネントのパターンをエクス ポートする args にコンポーネントのProps を渡す( 型安全!) 本例はデフォルトとローディングのパターンを指定

Slide 15

Slide 15 text

Storybook の dev server を起動すると ブラウザで(localhost:6006 などで) 専用の画面が立ち上がる! 画面左ペインにパターン別にコンポーネントが並んでいる

Slide 16

Slide 16 text

Storybook の dev server を起動すると② パターンを切り替えると見た目が変わる

Slide 17

Slide 17 text

もっと詳しいことは 公式ドキュメント https://storybook.js.org/docs/react/get-started/introduction ただし、Storybook6.4 からストーリーの書き方 (Component Story Format) が 3.0 となり大幅に改善されたた め、以下ブログを最初に読み、改善後の書き方を覚えた上で読むべし https://storybook.js.org/blog/component-story-format-3-0/ 本スライドで紹介しているのはCSF3.0 です

Slide 18

Slide 18 text

React Native アプリへの Storybook 導入

Slide 19

Slide 19 text

React Native アプリへの Storybook 導入の壁 Storybook は基本的にはWeb ブラウザ上で動く React Native のコンポーネントはそのままでは動作しない 策は2 つ Storybook でのみ react-native-web としてコンポーネントを扱う @storybook/react-native を使う 順に説明します ` ` ` `

Slide 20

Slide 20 text

react-native-web としてコンポーネントを扱う① 全体感 Storybook 実行時のみ、 react-native を react-native-web として扱う Storybook 自体はReact アプリケーションを扱っているかのようにセットアップする セットアップコマンド ` ` ` ` npx -p @storybook/cli sb init --type react

Slide 21

Slide 21 text

react-native-web としてコンポーネントを扱う② React Native Web addon for Storybook をインストールします .storybook/main.js に追記 ` ` yarn add babel-plugin-react-native-web @storybook/addon-react-native-web --dev ` ` addons: [ '@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions', // 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'], }, }, ],

Slide 22

Slide 22 text

react-native-web としてコンポーネントを扱う③ NativeBase を使っているなど、共通のContextProvider が必要な場合は 同様に生成される .storybook/preview.js も追記する Decorator という、Storybook の機能を使っています (https://storybook.js.org/docs/react/writing-stories/decorators) ` ` const withThemeProvider = (Story, context) => { return ( ); }; export const decorators = [withThemeProvider]; ` `

Slide 23

Slide 23 text

react-native-web としてコンポーネントを扱う [ まとめ ] @storybook/addon-react-native-web の作者によって以下の記事にまとめられています。 https://levelup.gitconnected.com/introducing-react-native-web-storybook-3c56d48a6508 メリット 最新のAddon が利用できるよ Chromatic などのVRT がサポートされているよ 静的ファイルとして簡単に公開できるよ 最新のStorybook の機能が全部使えるよ デメリット react-native-web 未サポートのライブラリは使えないよ ネイティブデバイスと同じ動きは再現できないよ どうしてもネイティブの動きを再現したければ @storybook/react-native を使いましょう ` ` ` `

Slide 24

Slide 24 text

余談: @storybook/addon-react-native-web を読む https://github.com/storybookjs/addon-react-native- web/blob/ffe28c7faae2e1fea5e1757826c0c6848a71be7f/src/webpack.ts react-native を react-native-web にエイリアスしたり、 react-native-web 特有の拡張子の .web.js などをresolve のターゲットに入れたりしてます。 @storybook/addon-react-native-web を使わ なくても、これらの設定を .storybook/main.js に書けばいけるかも。 config.resolve.extensions = [ '.web.js', '.web.jsx', '.web.ts', '.web.tsx', ...config.resolve.extensions, ]; config.resolve.alias = { 'react-native$': 'react-native-web', ...config.resolve.alias, ...userAliases, }; ` ` ` ` ` ` ` ` ` ` ` `

Slide 25

Slide 25 text

余談②:うまく動かない実例 (当然だが)アラートなどネイティブ独自のUI は再現できない react-navigation の useNavigation を使っているコンポーネントがStorybook 上で表示できていない (自社の環境でのみ再現) https://davidl.fr/blog/react-navigation-object-storybook の内容をやってみてもダメだった ` ` ` `

Slide 26

Slide 26 text

@storybook/react-native を使う ネイティブデバイス上でStorybook を確認できるプラグイン しかし基本的に開発がWeb のStorybook より遅れている 2022 年5 月30 日時点では v6.0.1-beta.6 が出ているが、本家は v6.5 以上 セットアップ手順は以下 https://github.com/storybookjs/react-native/blob/v6.0.1-beta.6/MANUAL_SETUP.md ・・・らしいけど自分の手元で動かしてもCSF3.0 では動作しませんでした 自分の見解 現状、Storybook を使うのならば、割り切ってreact-native-web Addon を使うのがよさそう Storybook 公式Discord のreact-native チャンネルで @storybook/react-native の更新情報が流れているの で、気になる方は引き続きウォッチしていると進展があるかも ` ` ` ` ` `

Slide 27

Slide 27 text

MSW を使ってネットワークをモックする

Slide 28

Slide 28 text

概要 コンポーネントからGET したりPOST したり、ネットワークにアクセスする場合がある 一案としてネットワークへアクセスするコンポーネントと、その結果やハンドラのみProps 経由でやり取り するコンポーネントに切り分けて、後者だけStorybook を使う方法もある もう一案として、ネットワークへのアクセスをモックできるMSW(Mock Service Worker) を使って、ネット ワークごとStorybook で表現する方法がある msw-storybook-addon を使う ` `

Slide 29

Slide 29 text

今回 MSW を使いたいコンポーネント 検索窓に単語を入れると、先生や科目名がサジェストされる。サジェスト内容はAPI を介してGET される。 コンポーネントの性質上、API へのGET した結果に沿ってサジェストされるところまで動きを見れてナンボ

Slide 30

Slide 30 text

今回 MSW を使いたいコンポーネント ( 一部 ) export const useSuggestField: (props: useHooksParams) => useHooksReturn = (props) => { const [value, setValue] = useState(''); const [suggestItems, setSuggestItems] = useDebounce([]); useEffect(() => { let canceled = false; apiClient.teachers.suggests.$get({ query: { keyword: value } }).then((res) => { if (!canceled) { if (res.length === 0) { setSuggestItems([]); } setSuggestItems(res); } }); return () => { canceled = true; }; }, [value, setSuggestItems]); const onInput = useCallback(async (str: string) => { setValue(str); aspida を使って、「/teachers/suggests 」というAPI にValue が変わるたびにGET リクエストを飛ばし、そ の結果をサジェストアイテムにセットしている 余談だがキャンセル処理に注意

Slide 31

Slide 31 text

MSW を使ったストーリー① export const SearchSubject: ComponentStoryObj = { args: { onSubmit: () => undefined, }, play: async () => { const input = screen.getByLabelText(' 科目名や先生名を入力するテキストフィールド'); await userEvent.type(input, ' 英語', { delay: 200, }); }, parameters: { msw: { handlers: [ rest.get('/teachers/suggests', (req, res, ctx) => { return res( ctx.json([ { target: 'subjects', item_id: 'english', name: ' 英語', }, ] as AutocompleteItem[]), ); play 関数を指定すると、テキストフィールドに文字 を入力させるなどの操作ができる userEvent.type メソッドで「英語」という文字を200 ミリ秒遅らせて( リアルに) 入力させる

Slide 32

Slide 32 text

MSW を使ったストーリー② export const SearchSubject: ComponentStoryObj = { args: { onSubmit: () => undefined, }, play: async () => { const input = screen.getByLabelText(' 科目名や先生名を入力するテキストフィールド'); await userEvent.type(input, ' 英語', { delay: 200, }); }, parameters: { msw: { handlers: [ rest.get('/teachers/suggests', (req, res, ctx) => { return res( ctx.json([ { target: 'subjects', item_id: 'english', name: ' 英語', }, ] as AutocompleteItem[]), ); 今回の例は、「/teachers/suggests 」というAPI に GET リクエストがあれば、それをIntercept して( 遮っ て) 補完内容をレスポンスする、という意味 細かい書き方はMSW とAddon 固有なので覚える

Slide 33

Slide 33 text

MSW を組み込んだ結果 ユーザーの入力と、それに応じてAPI を叩いてサジェストする様子を、コンポーネント単体でシミュレートで きるようになった!

Slide 34

Slide 34 text

Scaffdog でチーム運用に乗せる

Slide 35

Slide 35 text

Story をいちいち書くのは地味に面倒 よく見るのはコンポーネントと同じディレクトリに ComponentName.stories.tsx という名前でストーリ ーを置く いちいちコンポーネントを作るたびに同じような作業をしてストーリーを作るのは地味に面倒 ` `

Slide 36

Slide 36 text

Scaffdog がおすすめ https://github.com/cats-oss/scaffdog 簡単にいうと・・・ ファイルのひな形を作っておくと、コマンド一発でひな形に沿ったファイルを作成、配置してくれる Markdown でひな形を書く 同時に複数のファイルをひな形に沿って一発生成できる 複数種類のファイルを、同じような形式でチームで統一して作ってもらいたいときに便利!

Slide 37

Slide 37 text

マークダウンのひな形の例 複数のファイルを見出しで切り分けてひな形を指定できる コンポーネント、Storybook 、(必要なら)jest ファイル... といった感じで一度にまとめて書ける

Slide 38

Slide 38 text

実際に使ってみる様子 コマンド叩いて、ディレクトリ選んで、コンポーネント名をEnter 。

Slide 39

Slide 39 text

以下のような Story が自動生成される import type { ComponentMeta, ComponentStoryObj } from '@storybook/react'; import { Button } from './'; export default { component: Button, } as ComponentMeta; export const Default: ComponentStoryObj = { args: { }, };

Slide 40

Slide 40 text

まとめ

Slide 41

Slide 41 text

まとめ @storybook/addon-react-native-web を使うのが現状はおすすめ ただし、 useNavigation を内部で使っていると動作しない→動いたという企業さんがいたので、バー ジョン等の兼ね合いかも? MSW でAPI アクセスをモックできるのが便利 Scaffdog で必要なファイルの自動生成させると運用に乗せやすいかも 告知 オンライン家庭教師マナリンクではエンジニア採用やってます! React Native でオンライン指導アプリを開発したい方はお声がけください Twitter( 名人|マナリンクCTO / @meijin_garden) よかったらフォローしてね ` ` ` `