0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Chakra UI v3 × React Hook Form でバリデーション

0
Posted at

【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 への登録部分は省略しています)。

Register.tsx
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 も使っていました。

NG例
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 で見ている項目名が違っていた

NG例
<Field.Root invalid={!!errors.title}>   {/* title という項目は存在しない */}
    <Input {...register("user_id", ...)} />

register に渡した名前と errors.〇〇 の名前は一致させる必要があります。
useForm<FormData> で型を渡しておけば、存在しない名前を書いたときに TypeScript がエラーで教えてくれます。

エラーメッセージを固定の文字列にしていた

NG例
<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

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?