🦛

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

に公開
1

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

react-hook-formで管理されているフォームで変更があり、ブラウザバックやページ遷移でフォーム画面から離脱する際に「変更が破棄されます」という確認ダイアログを表示する判定としてisDirtyを利用することを想定しています。

setValueでshouldDirty:true`にしない場合、isDirtyが動かない

setValueで値を更新する場合shouldDirty:trueにしないとisDirtyが動きません。これに関しては以下のissueで取り上げられています。

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

もしshouldDirty:trueにしても解決しない場合は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.

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

少し脱線しますが、上記リンクの公式ドキュメントにサンプルコード(以下コード)があるのですが、こちらはshouldDirtyが追加されていないためisDirtyがうまく動かないので注意してください。動作確認は2025年4月26日に実施しています。

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

// isDirty: true ✅
setValue('test', 'change')

// isDirty: false because there getValues() === defaultValues ❌
setValue('test', '')

「なぜdefaultValuesを設定しないとisDirtyがうまく動かないのか?」と疑問に思うかもしれません。こちらはサンプルコードのコメントアウトの通りgetValues() === defaultValuesであるからで、実際にreact-hook-formのコードを見てみるとisDirtyの結果を得るために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 },
  setValue,
} = useForm({ defaultValues: { test: undefined } });

調査をしてみたところ、どうやら「defaultValuesはundefinedを期待していないから」が理由になりそうです。念の為react-hook-formで定義されているdeepEqualのコードを見てみましたがundefinedがある場合の考慮されていなさそうです。そもそも比較の対象がないと判定のしようがないのかもしれませんね。

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

では、やむなくdefaultValuesのフィールドにundefinedを含むが、それでもisDirtyを使いたい場合はどうすればよいのでしょうか?

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

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

dirtyFieldsに関しては以下のIssueで取り上げられていましたので、詳細が気になる方がいらっしゃいましたらご覧ください。

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

実際に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を使用する際は以下を念頭に実装を進める必要がありそうです。

  • isDirtyを判定するために必要な比較対象(defaultValues)を設定する
  • setValueで更新する場合はshouldDirty:trueにする
  • defaultValuesにundefinedを設定しない
  • defaultValuesにundefinedを設定する場合はdirtyFieldsを使う

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

追記

社内向けにreact-hook-formのsetValueにisDirtyを強制するカスタムルールを作りました。気が向いたらパブリックにしようと思っています。

https://zenn.dev/shuuuuuun/articles/a28ab461a4df13

参考記事

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

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

Discussion