MeWrite Docs

React Hook Form validation not working

React Hook Formのバリデーションが正しく動作しない場合の対処法

概要

React Hook Formでバリデーションが期待通りに動作しない場合の原因と解決策です。

エラーメッセージ

明示的なエラーは出ませんが、以下の症状が発生します:

  • バリデーションエラーが表示されない
  • formState.errorsが常に空
  • handleSubmitが動作しない

解決策

1. 基本的なセットアップ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { useForm } from 'react-hook-form'

interface FormData {
  email: string
  password: string
}

function LoginForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormData>()

  const onSubmit = (data: FormData) => {
    console.log(data)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register('email', {
          required: 'Email is required',
          pattern: {
            value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
            message: 'Invalid email address',
          },
        })}
      />
      {errors.email && <span>{errors.email.message}</span>}

      <input
        type="password"
        {...register('password', {
          required: 'Password is required',
          minLength: {
            value: 8,
            message: 'Password must be at least 8 characters',
          },
        })}
      />
      {errors.password && <span>{errors.password.message}</span>}

      <button type="submit">Submit</button>
    </form>
  )
}

2. Zodとの統合

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'

const schema = z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
})

type FormData = z.infer<typeof schema>

function LoginForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormData>({
    resolver: zodResolver(schema),
  })

  // ...
}

3. エラーが表示されない場合

1
2
3
4
5
6
7
8
9
// errorsを正しく参照しているか確認
const { formState: { errors } } = useForm()

// NG: formStateを分割代入せずに使用
const { formState } = useForm()
// formState.errorsは更新されない場合がある

// OK: errorsを直接分割代入
const { formState: { errors } } = useForm()

4. バリデーションモードの設定

1
2
3
4
5
6
7
const { register, handleSubmit, formState: { errors } } = useForm({
  mode: 'onChange',      // 入力時にバリデーション
  // mode: 'onBlur',     // フォーカスが外れた時
  // mode: 'onSubmit',   // 送信時のみ(デフォルト)
  // mode: 'onTouched',  // 最初のblur後、入力時にも
  // mode: 'all',        // onChange + onBlur
})

5. デフォルト値の設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const { register } = useForm({
  defaultValues: {
    email: '',
    password: '',
  },
})

// または非同期で設定
const { reset } = useForm()

useEffect(() => {
  fetchUser().then((user) => {
    reset(user)
  })
}, [reset])

6. コントロールドコンポーネントとの統合

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Controller, useForm } from 'react-hook-form'
import { TextField } from '@mui/material'

function Form() {
  const { control, handleSubmit } = useForm()

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="email"
        control={control}
        rules={{ required: 'Email is required' }}
        render={({ field, fieldState: { error } }) => (
          <TextField
            {...field}
            error={!!error}
            helperText={error?.message}
          />
        )}
      />
    </form>
  )
}

7. 非同期バリデーション

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const { register } = useForm()

<input
  {...register('username', {
    validate: async (value) => {
      const isAvailable = await checkUsername(value)
      return isAvailable || 'Username is already taken'
    },
  })}
/>

8. フォーム全体のエラー

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const { setError, clearErrors, formState: { errors } } = useForm()

const onSubmit = async (data) => {
  try {
    await submitForm(data)
  } catch (error) {
    setError('root', {
      type: 'manual',
      message: 'Form submission failed',
    })
  }
}

// 表示
{errors.root && <div>{errors.root.message}</div>}

関連エラー

関連エラー

React の他のエラー

最終更新: 2025-12-19