【React】Chakra UI v3 × React Hook Form でバリデーションとエラーメッセージを表示する
はじめに
名刺登録アプリの登録フォームを作っていて、最初は useState で入力値を管理していました。
バリデーションとエラーメッセージを表示したくなって React Hook Form を入れたところ、useState と React Hook Form が混在してしまい、うまく動きませんでした。
この記事では、フォームの状態管理を React Hook Form に一本化して、Chakra UI v3 の Field コンポーネントでエラーメッセージを表示するまでの手順をまとめます。
環境
| ライブラリ | バージョン |
|---|---|
| React | 19.2 |
| TypeScript | 6.0 |
| Vite | 8.3 |
| @chakra-ui/react | 3.37 |
| react-hook-form | 7.89 |
インストール
npm install react-hook-form
完成形
先に完成したコードを載せます(Supabase への登録部分は省略しています)。
import { Card, Button, Stack, Field, Input, Textarea, NativeSelect } from "@chakra-ui/react"
import { useForm, type SubmitHandler } from "react-hook-form";
// フォームの型
type FormData = {
user_id: string,
name: string,
description: string,
skill_id: number,
}
function Register() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormData>({
defaultValues: {
user_id: '',
name: '',
description: '',
},
});
// バリデーションを通過したときだけ呼ばれる
const onSubmit: SubmitHandler<FormData> = async (formData) => {
console.log('送信された値:', formData);
// ここで API や Supabase に登録する
};
return (
<Card.Root width="350px">
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<Card.Body>
<Stack gap="4" w="full">
{/* ID:必須 + 半角英字のみ */}
<Field.Root required invalid={!!errors.user_id}>
<Field.Label>ID(好きな英単語)<Field.RequiredIndicator /></Field.Label>
<Input placeholder="example"
{...register("user_id", {
required: "IDの入力は必須です",
pattern: {
value: /^[a-zA-Z]+$/,
message: "IDは半角英字で入力してください",
},
})} />
<Field.ErrorText>{errors.user_id?.message}</Field.ErrorText>
</Field.Root>
{/* お名前:必須 */}
<Field.Root required invalid={!!errors.name}>
<Field.Label>お名前<Field.RequiredIndicator /></Field.Label>
<Input placeholder="山田 太郎"
{...register("name", {
required: "お名前の入力は必須です",
})} />
<Field.ErrorText>{errors.name?.message}</Field.ErrorText>
</Field.Root>
{/* 自己紹介:必須(Textarea) */}
<Field.Root required invalid={!!errors.description}>
<Field.Label>自己紹介<Field.RequiredIndicator /></Field.Label>
<Textarea
{...register("description", {
required: "自己紹介の入力は必須です",
})} />
<Field.ErrorText>{errors.description?.message}</Field.ErrorText>
</Field.Root>
{/* スキル:必須(select・数値で保持) */}
<Field.Root required invalid={!!errors.skill_id}>
<Field.Label>好きなスキル<Field.RequiredIndicator /></Field.Label>
<NativeSelect.Root>
<NativeSelect.Field placeholder="選択してください"
{...register("skill_id", {
required: "スキルを選択してください",
valueAsNumber: true,
})}>
<option value="1">React</option>
<option value="2">TypeScript</option>
</NativeSelect.Field>
<NativeSelect.Indicator />
</NativeSelect.Root>
<Field.ErrorText>{errors.skill_id?.message}</Field.ErrorText>
</Field.Root>
</Stack>
</Card.Body>
<Card.Footer justifyContent="center">
<Button type="submit" loading={isSubmitting}>登録する</Button>
</Card.Footer>
</form>
</Card.Root>
)
}
export default Register
ここから、ポイントごとに解説します。
ポイント1:useForm から必要なものを取り出す
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormData>({ defaultValues: { ... } });
useForm<FormData> のように型を渡すと、register("user_id") のフィールド名や errors.user_id に型補完が効くようになります。
formState: { errors, isSubmitting } は分割代入の書き方で、次のコードと同じ意味です。
const form = useForm<FormData>();
const errors = form.formState.errors;
const isSubmitting = form.formState.isSubmitting;
formState はフォーム全体の「今の状態」をまとめたオブジェクトです。
| 名前 | 中身 |
|---|---|
errors |
バリデーションエラーの情報(エラーがある項目だけ入る) |
isSubmitting |
onSubmit の実行中は true
|
isDirty / isValid / isSubmitted など |
変更されたか・全項目が正しいか・送信済みか |
今回は isSubmitting を Chakra UI の Button の loading に渡して、送信中はローディング表示にしています(二重送信の防止にもなります)。
ポイント2:register で入力欄とバリデーションルールをつなぐ
<Input
{...register("user_id", {
required: "IDの入力は必須です",
pattern: {
value: /^[a-zA-Z]+$/,
message: "IDは半角英字で入力してください",
},
})} />
register は name / onChange / onBlur / ref を返すので、スプレッド構文で Chakra UI の Input にそのまま渡せます。
そのため、useState や value / onChange を自分で書く必要はありません。
よく使うルールはこちらです。
| ルール | 例 | 内容 |
|---|---|---|
required |
required: "必須です" |
未入力ならエラー |
pattern |
{ value: /正規表現/, message: "..." } |
正規表現に合わなければエラー |
minLength / maxLength
|
{ value: 10, message: "..." } |
文字数の下限・上限 |
min / max
|
{ value: 0, message: "..." } |
数値の下限・上限 |
validate |
(v) => v !== "admin" || "使えないIDです" |
自由にチェック関数を書ける |
ポイント3:errors の中身と .message
ID を空のまま送信すると、errors には次のようなデータが入ります。
errors = {
user_id: {
type: "required", // どのルールに引っかかったか
message: "IDの入力は必須です", // register で書いたメッセージ
ref: <input要素>,
},
}
message には register のルールに書いた文字列がそのまま入ります。
エラーがない項目は errors.user_id 自体が undefined になるので、オプショナルチェーン ?. を使って取り出します。
{errors.user_id?.message}
中身を確認したいときは、コンポーネント内に console.log(errors) を書いて空のまま送信してみるとわかりやすいです。
ポイント4:Chakra UI v3 の Field でエラーを表示する
Chakra UI v3 では、Field.Root に invalid を渡すと入力欄が赤枠になり、Field.ErrorText が表示されます。
<Field.Root required invalid={!!errors.user_id}>
<Field.Label>ID<Field.RequiredIndicator /></Field.Label>
<Input {...register("user_id", { required: "IDの入力は必須です" })} />
<Field.ErrorText>{errors.user_id?.message}</Field.ErrorText>
</Field.Root>
-
invalid={!!errors.user_id}:エラーオブジェクトがあればtrueになる(!!で boolean に変換) -
Field.ErrorText:invalidがtrueのときだけ表示される -
required+Field.RequiredIndicator:ラベルに必須マーク(*)を表示する
Field.Root の required は必須マークの表示などに使われるもので、エラーメッセージを出すチェック自体は register の required で行っています。両方書き忘れないようにしましょう。
ポイント5:送信は handleSubmit(onSubmit) に任せる
const onSubmit: SubmitHandler<FormData> = async (formData) => {
// formData には入力値がまとめて入っている
};
<form onSubmit={handleSubmit(onSubmit)} noValidate>
-
handleSubmitがe.preventDefault()とバリデーションをまとめて実行してくれます -
onSubmitはバリデーションを通過したときだけ呼ばれます -
noValidateをつけると、ブラウザ標準のバリデーション(吹き出しのポップアップ)が止まり、Chakra UI 側のエラー表示に統一できます
ポイント6:select の値を数値で受け取る(valueAsNumber)
DB に skill_id を数値で登録したい場合でも、<select> の値はブラウザ上では常に文字列("1")です。
valueAsNumber: true をつけると、React Hook Form が数値に変換してから保持してくれます。
<NativeSelect.Field placeholder="選択してください"
{...register("skill_id", {
required: "スキルを選択してください",
valueAsNumber: true,
})}>
これで型定義も skill_id: number のまま扱え、送信時に Number() で変換する必要がなくなります。
「選択してください」のまま(値が空)の場合は、required のエラーが表示されます。
ハマったところ
useState と React Hook Form を混在させていた
最初は次のように、useState で値を管理しつつ register も使っていました。
const [formData, setFormData] = useState({ user_id: '' });
const { register, formState: { errors } } = useForm();
<Input value={formData.user_id} {...register("user_id", { required: "必須です" })} />
<form onSubmit={handleSubmitByMyself}>
これだと、
-
valueをuseStateで固定しているのにonChangeはregister側が使うため、入力しても値が変わらない - 自前の送信関数を使っているので、React Hook Form のバリデーションが実行されない
という状態になります。値の管理は React Hook Form に任せて、useState と value / onChange は削除するのが正解でした。
invalid で見ている項目名が違っていた
<Field.Root invalid={!!errors.title}> {/* title という項目は存在しない */}
<Input {...register("user_id", ...)} />
register に渡した名前と errors.〇〇 の名前は一致させる必要があります。
useForm<FormData> で型を渡しておけば、存在しない名前を書いたときに TypeScript がエラーで教えてくれます。
エラーメッセージを固定の文字列にしていた
<Field.ErrorText>内容の入力は必須です</Field.ErrorText>
これだと、pattern など別のルールのエラーでも同じ文言が出てしまいます。{errors.user_id?.message} にすると、引っかかったルールのメッセージが表示されます。
まとめ
- フォームの値は
useStateではなく React Hook Form のregisterで管理する - バリデーションルールとエラーメッセージは
registerの第2引数に書く - エラー内容は
formState.errors.項目名.messageに入る - Chakra UI v3 では
Field.RootのinvalidとField.ErrorTextでエラーを表示する - 送信は
handleSubmit(onSubmit)に任せ、noValidateでブラウザ標準のポップアップを止める - 数値で受け取りたいときは
valueAsNumber: true