🦛

【React】react-hook-formのisDirtyの挙動が思うようにならない

2024/04/29に公開

何回煎じられたかわかりませんが、react-hook-formのisDirtyの挙動について色々調べたのでアウトプットしていきます。関連しそうなTipsも載せていますので是非最後までご覧ください。

ユースケースとしてはreact-hook-formで管理されているフォームで何かしらの変更があり、ユーザーがブラウザバックやページ遷移でフォーム画面から離脱する場合に確認ダイアログを表示させます。確認ダイアログを表示する判定としてisDirtyを活用することを想定しています。

setValueはshouldDirty:trueにしないとisDirtyが動かない

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', '')

https://react-hook-form.com/docs/useform/formstate

「なぜdefaultValuesを設定しないとdirtyがうまく動かないのか?」に関しては上記のサンプルコードのコメントアウト以下の通りdefaultValuesと比較しているからです。実際にreact-hook-formのコードを見てみるとdefaultValuesと比較しているのがわかります。

https://github.com/react-hook-form/react-hook-form/blob/7cdad77ae13190e6b491d9b4b30d89709229b52b/src/logic/createFormControl.ts#L508-L511

defaultValuesのフィールドにundefinedが含まれているとisDirtyが期待した動きにならない

公式ドキュメントに倣ってuseFormのdefaultValuesを設定してもフィールドにundefinedが含まれている場合、isDirtyは期待通りに動きません。

const {
  formState: { isDirty, dirtyFields },
  setValue,
} = useForm({ defaultValues: { test: undefined } });

調査をしてみたところ、どうやらdefaultValuesはundefinedを期待していないからが理由になりそうです。(有識者が明言されているエビデンスを見つけられなかったので少し不安ではありますが...)念の為deepEqualのコードを見てみましたがundefinedがある場合の考慮されていなさそうです。そもそも比較の対象がないと判定のしようがないのかもしれませんね。

https://github.com/react-hook-form/react-hook-form/blob/7cdad77ae13190e6b491d9b4b30d89709229b52b/src/utils/deepEqual.ts

ではやむなくdefaultValuesのフィールドにundefinedを含むがそれでもisDirtyの挙動を享受したい場合はどうすればよいのでしょうか?そのような場合はdirtyFieldsを使って対応します。

isDirtyはフォーム全体の状態を判断したい場合に使いますが、dirtyFieldsを使うことでフィールドレベルでフォームの状態を判断します。以下のように実装するとisDirtyが期待する動作に近いものになります。

isDirty = Object.keys(dirtyFields).length > 0

コードは以下を参照しています。

isDirty changing to true when no field is actually dirty · Issue #3213 · react-hook-form/react-hook-form

react-hook-formのdirtyFieldsのコードは以下のようになっており、変更がある場合は{ key: true }としてdirtyFieldsに追加されます。

https://github.com/react-hook-form/react-hook-form/blob/7cdad77ae13190e6b491d9b4b30d89709229b52b/src/logic/getDirtyFields.ts

まとめ

ここまででざっくりとreact-hook-formのisDirtyについて触れてきましたが、isDirtyを使用する際は以下を念頭に実装を進める必要がありそうです。

  • 比較対象になるdefaultValuesを必ず設定する
  • setValueで更新する場合はshouldDirty: trueにする
  • defaultValuesのフィールドにundefinedを設定する必要がある場合はdirtyFieldsを使ってisDirtyに近い挙動を作る

簡単ですが以上になります。あまりisDirtyを使うケースは少ないのかもしれな以下もしれませんが、この記事がどなたかの助けになりましたら幸いです。以下はTipsになるのでスルーしていただいて問題ないです。

[Tips] 同期的にdefaultValuesの一部を変更したいときはresetFieldsのdefaultValuesオプションを使う

ユースケースとしてはcheckboxがdefaultValuesで管理されていて、selectboxで選択した値を参照してフィルタをかけられたcheckboxをdefaultValuesに再設定したい時など

https://react-hook-form.com/docs/useform/resetfield

ちなみに非同期にdefalutValuesをセットしたい場合はreset API + useEffectではなくvaluesを使用するのが良いそうです。

https://github.com/react-hook-form/react-hook-form/pull/9261

[Tips] useEffectでdirtyFieldsの変更を検知する際はformStateで管理する

以下issueを参考にしました(検証済み)

dirtyFields mutates and does not update when it changes · Issue #3711 · react-hook-form/react-hook-form

[Tips] handleSubmitはカリー化されている

こちらに関してはreact-hook-formのコードを見ている時に見つけたので表題とは全く関連しない内容になるのですが、handleSubmitがカリー化されていることに気づきました。普段、何気なく使っていましたが言われてみてばそうだよなと。カリー化の実例をあまり見かけたことがなかったので備忘録として残します。

// 普段は以下のように使っているけど
<Button onClick={handleSubmit(onSubmit, onError)}>Submit</Button>

// 実際にはこう使っていることになるよねってだけ
<Button onClick={(e) => handleSubmit(onSubmit, onError)(e)}>Submit</Button>

https://github.com/react-hook-form/react-hook-form/blob/7cdad77ae13190e6b491d9b4b30d89709229b52b/src/logic/createFormControl.ts#L1115-L1116

参考記事

https://github.com/react-hook-form/react-hook-form/issues/4740

https://github.com/react-hook-form/react-hook-form/issues/3213

Discussion