react-hook-form + zod resolver で useEffect 内での setError が動作しなかった時の対応メモ
前提・構成情報
- react 18.2
- react-hook-form 7.52.5
- zod 3.23.8
- @hookform/resolvers 3.9
メモ
<select>
要素でユーザーのロールを選択するようなシナリオがあるとする。
ロールはここでは admin
と user
の2値を選択出来るとする。
zod で以下のようなスキーマを定義する。
const FormSchema = z.object({
role: z.enum(["admin", "user"]),
})
useForm
の resolver プロパティにこのスキーマを与える。
const {
handleSubmit,
watch,
setError,
control,
formState: { errors },
} = useForm({
resolver: zodResolver(FormSchema),
defaultValues: {
role: "admin",
},
mode: "onChange",
})
この admin
ロールのユーザーには作成上限が存在しており、その上限に達している場合にはエラーを表示させたい、といったユースケースがある。
また、このフォームを持つコンポーネントは作成上限の値を props から受け取る。
useForm
の戻り値から取得した watch
関数を呼び出して role
フィールドの変更を監視する。
以下のような useEffect
を記述する。
useEffect(() => {
if (role === "admin" && limit > 5) {
setError("role", {
type: "manual",
message: "admin is not allowed to register",
})
}
}, [role])
admin
が選択されて、且つ limit
の条件を満たす場合 setError
が呼び出されることにより errors
オブジェクトが更新され、それを参照したのエラーメッセージが表示されることを期待していた。
しかし、この setError
は期待通りに動作しなかった。
色々と試行錯誤をした結果、setError
の実行後にどこかのタイミングで zod のバリデーションが実行されているのではないかと思い至った。(入力されている admin
という値は zod スキーマを満たすためエラーにならず、react-hook-form ではこのような場合 errors を更新する)
仮に zod バリデーションが動作しているのだとして、そのバリデーションよりも後に setError
が呼び出されればよいのかと考え、以下のように待機時間を 0
にした setTimeout
で setError
をラップしてみたところ期待通りにエラーが更新されることが分かった。
useEffect(() => {
if (role === "admin" && limit > 5) {
setTimeout(() => {
setError("role", {
type: "manual",
message: "admin is not allowed to register",
})
}, 0)
}
}, [role])
useForm
の呼び出し時に resolver を与えないようにするとこの現象は発生しなかった。(setTimeout
を使わずに setError
だけでエラーの状態が更新された)
useEffect
ではなく onChange
メソッドに割り込んで setError
を呼び出す場合でも同様の現象が発生した。
react-hook-form のリポジトリを見てみると resolver から与えたスキーマの実行は非同期処理になっており、これが期待とは異なる動作の原因になっているのではと考えたが、ソースコードの内容はほとんど読み解くことが出来なかった。
これを書いてから思いついた
こんな風にしてみたら良かったのかもしれない?
const formSchema = useMemo(() => {
return z.object({
role: z.string().refine(
(value) => !(limit && value === 'admin'),
{
message: "Admin selection is not allowed when limit is true",
}
),
});
}, [limit]);
Discussion