【React】react-hook-formのisDirtyの挙動が思うようにならない
何回煎じられたかわかりませんが、react-hook-formのisDirtyの挙動について色々調べたのでアウトプットしていきます。関連しそうなTipsも載せていますので是非最後までご覧ください。
ユースケースとしてはreact-hook-formで管理されているフォームで何かしらの変更があり、ユーザーがブラウザバックやページ遷移でフォーム画面から離脱する場合に確認ダイアログを表示させます。確認ダイアログを表示する判定としてisDirtyを活用することを想定しています。
shouldDirty:true
にしないとisDirtyが動かない
setValueはsetValueで値を更新する場合、shouldDirty:true
にしないとisDirtyが動きません。これに関しては以下のissueで取り上げられています。
setValue() doesn't dirty the form · Issue #72 · react-hook-form/react-hook-form
もし上記で解決しない場合はuseFormのdefaultValuesを設定していない可能性があります。公式ドキュメントを見ると注意事項に記載がありました。
Important: Make sure to provide all inputs' defaultValues at the useForm, so hook form can have a single source of truth to compare whether the form is dirty.
以下は公式ドキュメントから引用しているのですが、shouldDirtyオプションをつけていないのはなんでだろう。(下記では期待値にならず、shouldDirty: true
で期待値になることは検証済み)
const {
formState: { isDirty, dirtyFields },
setValue,
} = useForm({ defaultValues: { test: "" } });
// isDirty: true
setValue('test', 'change')
// isDirty: false because there getValues() === defaultValues
setValue('test', '')
「なぜdefaultValuesを設定しないとdirtyがうまく動かないのか?」に関しては上記のサンプルコードのコメントアウト以下の通りdefaultValuesと比較しているからです。実際にreact-hook-formのコードを見てみるとdefaultValuesと比較しているのがわかります。
defaultValuesのフィールドにundefinedが含まれているとisDirtyが期待した動きにならない
公式ドキュメントに倣ってuseFormのdefaultValuesを設定してもフィールドにundefinedが含まれている場合、isDirtyは期待通りに動きません。
const {
formState: { isDirty, dirtyFields },
setValue,
} = useForm({ defaultValues: { test: undefined } });
調査をしてみたところ、どうやらdefaultValuesはundefinedを期待していないからが理由になりそうです。(有識者が明言されているエビデンスを見つけられなかったので少し不安ではありますが...)念の為deepEqualのコードを見てみましたがundefinedがある場合の考慮されていなさそうです。そもそも比較の対象がないと判定のしようがないのかもしれませんね。
ではやむなくdefaultValuesのフィールドにundefinedを含むがそれでもisDirtyの挙動を享受したい場合はどうすればよいのでしょうか?そのような場合はdirtyFieldsを使って対応します。
isDirtyはフォーム全体の状態を判断したい場合に使いますが、dirtyFieldsを使うことでフィールドレベルでフォームの状態を判断します。以下のように実装するとisDirtyが期待する動作に近いものになります。
isDirty = Object.keys(dirtyFields).length > 0
コードは以下を参照しています。
react-hook-formのdirtyFieldsのコードは以下のようになっており、変更がある場合は{ key: true }
としてdirtyFieldsに追加されます。
まとめ
ここまででざっくりとreact-hook-formのisDirtyについて触れてきましたが、isDirtyを使用する際は以下を念頭に実装を進める必要がありそうです。
- 比較対象になるdefaultValuesを必ず設定する
- setValueで更新する場合は
shouldDirty: true
にする - defaultValuesのフィールドにundefinedを設定する必要がある場合はdirtyFieldsを使ってisDirtyに近い挙動を作る
簡単ですが以上になります。あまりisDirtyを使うケースは少ないのかもしれな以下もしれませんが、この記事がどなたかの助けになりましたら幸いです。以下はTipsになるのでスルーしていただいて問題ないです。
[Tips] 同期的にdefaultValuesの一部を変更したいときはresetFieldsのdefaultValuesオプションを使う
ユースケースとしてはcheckboxがdefaultValuesで管理されていて、selectboxで選択した値を参照してフィルタをかけられたcheckboxをdefaultValuesに再設定したい時など
ちなみに非同期にdefalutValuesをセットしたい場合はreset API + useEffectではなくvaluesを使用するのが良いそうです。
[Tips] useEffectでdirtyFieldsの変更を検知する際はformStateで管理する
以下issueを参考にしました(検証済み)
[Tips] handleSubmitはカリー化されている
こちらに関してはreact-hook-formのコードを見ている時に見つけたので表題とは全く関連しない内容になるのですが、handleSubmitがカリー化されていることに気づきました。普段、何気なく使っていましたが言われてみてばそうだよなと。カリー化の実例をあまり見かけたことがなかったので備忘録として残します。
// 普段は以下のように使っているけど
<Button onClick={handleSubmit(onSubmit, onError)}>Submit</Button>
// 実際にはこう使っていることになるよねってだけ
<Button onClick={(e) => handleSubmit(onSubmit, onError)(e)}>Submit</Button>
参考記事
Discussion
感謝🙏