React Hook Form をデコレーターとして用いた Storybook を実装する
背景
社内 の Next.js で実装している Web サービスのフォーム部分を React Hook Form を用いています。一方、各コンポーネントのリスト化し確認するのに Storybook を用いています。
React Hook Form のコンテキストを含むコンポーネントを Storybook で表示するのにデコレーターが必要だったので備忘録としてまとめておきます。
同じようなことをしようとしている方の助けになれば幸いです。
ソフトウェアのバージョン
$ npm ls | grep next
├── @storybook/nextjs@7.5.3
├── eslint-config-next@13.4.19
├── next-auth@4.24.7
├── next@13.5.6
$ npm ls | grep react-hook-form
├── react-hook-form@7.47.0
$ npm ls | grep storybook
├── @storybook/addon-actions@7.6.17
├── @storybook/addon-essentials@7.5.3
├── @storybook/addon-interactions@7.5.3
├── @storybook/addon-links@7.5.3
├── @storybook/addon-onboarding@1.0.8
├── @storybook/blocks@7.5.3
├── @storybook/nextjs@7.5.3
├── @storybook/react@7.5.3
├── @storybook/testing-library@0.2.2
├── eslint-plugin-storybook@0.6.15
├── storybook@7.5.3
何が起こっているか
React Hook Form は Next.js で簡単にフォームを実装するのをサポートするライブラリです。
社内のサービスでも入力値のバリデーションや初期値のアサインなどを行なっています。
React Hook Form が用意している Hook を利用したい場合は、利用する予定のコンポーネントを FormProvider
でネストしてあげる必要があります。
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(() => onSubmit())}>
<SingleText />
</form>
</FormProvider>
)
以上の例の場合 SingleText
コンポーネントで React Hook Form の Hook などを利用できるようになります。
一方、 Storybook とは各コンポーネントがどのような見た目であるかを独立させて表示させて確認できるサービスです。開発者はこのページにはどういう見た目のどのコンポーネントを使うべきかなというのを Storybook 上で確認することができます。
この Storybook で自分が作成したコンポーネントを表示するのは以下のように実装したコンポーネントを呼び出してくる必要があります。
import type { Meta, StoryObj } from '@storybook/react'
import SingleText from '../app/components/SingleText'
export default {
title: 'Quest/SingleText',
component: SingleText,
parameters: {
layout: 'centered',
},
args: [
{
id: 1,
item: { '1': 'text' },
},
],
} as Meta<typeof SingleText>
export const Primary: StoryObj<typeof SingleText> = {
args: [
{
id: 1,
item: { '1': '' },
},
],
}
export const Secondary: StoryObj<typeof SingleText> = {
args: [
{
id: 1,
item: { '1': 'SingleText の初期値です' },
},
],
}
しかし、繰り返しになりますが React Hook Form を利用しているコンポーネントの場合、 FormProvider
でネストされている必要があります。上記のコードはネストされていないので実行すると以下のように少し見えにくいですが React Hook Form に関するエラーが出ます。
まとめると
- React Hook Form :
FormProvider
にネストされている状態で使う必要がある - Storybook : コンポーネントを独立させて表示したい
というお互いの事情がうまくマッチしていない状態になってしまっていると言えます。
方法
解決方法としてこちらの gist に有志の方がまとめてくださっているのを見つけることができました。
- Storybook と同じディレクトリに
withRHT.tsx
というコンポーネントを実装します。
import { action } from '@storybook/addon-actions'
import { ReactNode, FC } from 'react'
import { FormProvider, useForm } from 'react-hook-form'
function StorybookFormProvider({
children,
}: {
children: ReactNode
}): JSX.Element {
const methods = useForm()
return (
<FormProvider {...methods}>
<form
onSubmit={methods.handleSubmit(action('[React Hooks Form] Submit'))}
>
{children}
</form>
</FormProvider>
)
}
export default function withRHF(showSubmitButton: boolean) {
// eslint-disable-next-line func-names
return function (Story: FC) {
return (
<StorybookFormProvider>
<Story />
{showSubmitButton && <button type="submit">Submit</button>}
</StorybookFormProvider>
)
}
}
withRHF
コンポーネントはユニットテストでいうモックのような存在にあたる StorybookFormProvider
を呼び出しており、さらに StorybookFormProvider
は React Hook Form の FormProvider
を呼び出しています。
- React Hook Form を用いているコンポーネントの Storybook のデコレーターに
withRHF
を指定する
以下のようにデコレーターに withRHF
を指定します。 withRHF(false)
の false
は引数 showSubmitButton = false
と指定していると言えます。
import type { Meta, StoryObj } from '@storybook/react'
import SingleText from '../app/components/SingleText'
+ import withRHF from './withRHF'
export default {
title: 'Quest/SingleText',
component: SingleText,
parameters: {
layout: 'centered',
},
args: [
{
id: 1,
item: { '1': 'text' },
},
],
+ decorators: [withRHF(false)],
} as Meta<typeof SingleText>
export const Primary: StoryObj<typeof SingleText> = {
args: [
{
id: 1,
item: { '1': '' },
},
],
}
export const Secondary: StoryObj<typeof SingleText> = {
args: [
{
id: 1,
item: { '1': 'SingleText の初期値です' },
},
],
}
こうすることで Storybook で表示する時に SingleText
コンポーネントは withRHF
コンポーネントでネストされている扱いになり、実質 FormProvider
でネストされている状態と言えます。
これで以下のように Storybook 上でも描画することができました 🎉
💡 まとめ
- React Hook Form に対応したデコレーターを実装することができました
- React Hook Form を利用しているコンポーネントを Storybook に描画することができました
React Hook Form と Storybook はどちらも便利なライブラリであるため 「同時に使いたいけどなかなか同時に使った例が出てこないな」と苦労したので、まとめておきました。同じことで悩んでいる方の助けになれば幸いです。
Discussion