巨大で複雑なフォームをReactHookFormで作るときに気をつけたい1つのこと
概要
Reactでフォーム開発を行うとき、ReactHookFormが選択肢に入ってくるかなと思います。
筆者も直近の開発では、ReactHookFormを使ってフォーム開発を行っていました。とても巨大で複雑なフォームの開発を行っていたのですが、1つのアンチパターンを踏んでしまったので、記事化したいと思います。
tl;dr
結論から書くと「ReactHookFormのライフサイクルを Reactコンポーネントのライフサイクルに合わせるのが無難そう」と言うことです。
詳細
Reactコンポーネントのライフサイクルとは、mountやunmountと言った一連の流れになります。
参照:https://react.dev/learn/lifecycle-of-reactive-effects#recap
ReactHookFormもまた、このReactのライフサイクルの影響を受けます。
例えば、以下のコードについて考えます。(コードは簡略化して書いているので、実際には動作しない可能性があります)
import { useForm, useFieldArray } = 'react-hook-form'
const App: React.FC = () => {
const form = useForm()
form.watch((data) => {
console.log(data) // Todos の unmount 時にも値が流れてくる!
// unmount 時の不正なデータを弾く必要が出てしまった
if (isValidData(data)) {
// ここフォームデータを使った何かの処理
}
})
return (
<Todos form={form} />
)
}
const Todos: React.FC = ({ form }) => {
const items = useFieldArray({ control: form.control })
return (
// Todo一覧表示
)
}
このコードでは、Todosコンポーネントがunmountすると、Appで行っているwatchに値が流れ込んできます。しかも、フォームに入力されていた値ではなく、初期値のような空の配列が流れてきたりします。
そのため、不正な値を判定する処理が必要になってしまいました。もちろん、この判定処理がないと、後続の処理に不正なデータが流れ込んでしまいます。
この問題は、ReactHookFormのGithubのissueでたびたび議論されており、「想定通りの挙動である」と回答がついています。
この問題に関連してshouldUnregisterなどのプロパティが存在するらしいのですが、自分はうまく使いこなせず…。それよりも、本記事で書いているような「ライフサイクルを合わせるべし」との結論に達しました。
↓ unmountに関する数々のissue
実際には、自分たちのコードベースはもっと複雑です。
状態管理にReactのContextを利用しており、Context内でuseFormをしています。そのため、 ReactコンポーネントよりもReactHookFormのインスタンスが長生きします。これ自体は致命的な問題ではないかもしれません。
が、その結果としてwatchやresetを駆使するような、力技な実装になってしまいました。Contextは素直に状態管理をするに留めて(考えてみれば当たり前ですね)、ReactHookFormのことはコンポーネント側に寄せるのが無難だったなと今では思います。
しかしコードベースが膨らんだ今、この作りを変えることは難しく…。後悔と共にこの記事を執筆した次第となります。
終わりに
ReactHookFormを使ったフォーム開発における気づきについて書いてみました。
人によっては「当たり前じゃん」と思われるかもしれません。あるいはコードサンプルがシンプルすぎてピンとこない方もいらっしゃるかもしれません。が、備忘録的に残しておきます。
いやーReactHookFormは難しいですね。簡単な使い方ならともかく、複雑なフォームを開発していると、地味にハマることが多くて困りました。
Discussion