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?

Reactを学ぶ フォーム

Posted at

フォーム

フロントエンド開発において、フォームとはエンドユーザーからの入力を受け取る代表的な手段である。名前やメールアドレス、検索キーワード、アンケートの回答など、ユーザーが操作するあらゆる入力データは、フォームを通してアプリケーションに伝えられる。

なぜ必要なのか

フォームは、ユーザーから情報を受け取り、アプリケーションがその情報をもとに処理を行うために必要不可欠である。ログインや検索、注文、問い合わせなど、ユーザーがサービスを操作するための入口となる。フォームがなければ、アプリは一方的な表示にとどまり、対話的な機能を持てなくなる。

制御コンポーネント

制御コンポーネントとは、フォームの入力値を State で管理するコンポーネントのこと。ユーザーが入力するたびに State が更新され、その State に基づいて表示がレンダリングされる。

特徴

  • 入力値がリアルタイムに State と同期される
  • 入力内容の検証や条件による表示制御が容易
  • フォームの状態を一元管理できるため、複雑なフォームに向いている

コード例

「名前」と「年齢」を入力する制御コンポーネントのコード例を紹介する。

image.png

パターン①:useState() を分けて管理する

特徴

  • 各フィールドの状態が明確で、単純なフォームに向いている
  • 状態が増えると useState() の行数が増えて冗長になりやすい
export const StateForm: React.FC = () => {
    const [name, setName] = useState <string>('');
    const [age, setAge] = useState <string>('');

    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        alert(`名前: ${name}, 年齢: ${age}`);
    };

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label>名前:</label>
                <input
                    type="text"
                    value={name}
                    onChange={(e) => setName(e.target.value)}
                />
            </div>
            <div>
                <label>年齢:</label>
                <input
                    type="number"
                    value={age}
                    onChange={(e) => setAge(e.target.value)}
                />
            </div>
            <button type="submit">送信</button>
        </form>
    );
};

パターン②:useState() を1つにまとめて管理する

特徴

  • 状態が1つにまとまっていて、フォームの項目が多い場合に便利
  • onChange を 汎用化できる ので、同じロジックで複数フィールドに対応しやすい
type FormData = {
    name: string;
    age: string;
};

export const StateForm: React.FC = () => {
    const [form, setForm] = useState <FormData> ({
        name: '',
        age: '',
    });

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const { name, value } = e.target;
        setForm((prev) => ({ ...prev, [name]: value }));
    };

    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        alert(`名前: ${form.name}, 年齢: ${form.age}`);
    };

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label>名前:</label>
                <input
                    type="text"
                    name="name"
                    value={form.name}
                    onChange={handleChange}
                />
            </div>
            <div>
                <label>年齢:</label>
                <input
                    type="number"
                    name="age"
                    value={form.age}
                    onChange={handleChange}
                />
            </div>
            <button type="submit">送信</button>
        </form>
    );
};

useState() vs useImmer()

制御コンポーネントでは、フォームの状態を useState() で管理するのが基本だが、オブジェクトが入れ子構造になると記述が煩雑になりやすい。その場合、useImmer() を使うとよりシンプルに状態更新ができる。

useState() の場合(ネストあり)

特徴

  • スプレッド構文が多くなり、ネストが深くなるほど書きづらくなる
  • 更新ミスが起きやすく、読みづらい
type FormData = {
    user: {
        name: string;
        age: string;
    };
};

const [form, setForm] = useState<FormData>({
    user: {
        name: '',
        age: '',
    },
});

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setForm((prev) => ({
        ...prev,
        user: {
            ...prev.user,
            [name]: value,
        },
    }));
};

スプレッド構文によるオブジェクトのコピーは浅いコピーのため、ネストに弱い。

useImmer() の場合

特徴

  • スプレッド構文不要で、直接オブジェクトを更新するように書ける
  • ネストが深くても直感的に扱える
  • useState() よりも複雑なフォーム構造に向いている
import { useImmer } from 'use-immer';

type FormData = {
    user: {
        name: string;
        age: string;
    };
};

const [form, setForm] = useImmer<FormData>({
    user: {
        name: '',
        age: '',
    },
});

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setForm((form) => {
        form.user[name as 'name' | 'age'] = value;
    });
};

setForm((form) => { ... })form は、Immer による「編集用のコピー」。見た目は普通のオブジェクトだが、直接書き換えてOK

非制御コンポーネント

非制御コンポーネントとは、フォームの入力値を State で保持しないコンポーネントのことを。State を用いないため、入力値が欲しい場合は、DOM の参照を通じて直接値を取得する。

特徴

  • 入力値の変更時に State 更新が発生しないため、再レンダリングが減りパフォーマンスに有利
  • 実装がシンプルで、簡単なフォームに適している
  • 入力値を取得するタイミングが限られ、リアルタイム検証には向かない
  • State と同期しないため、UI の一元管理が難しい場合がある

コード例

特徴

  • useRef() を使って DOM に直接アクセスし、値を取得する
  • 初期値の設定に必ず defaultValue を使う必要がある
export const UncontrolledForm: React.FC = () => {
    const nameRef = useRef<HTMLInputElement>(null);
    const ageRef = useRef<HTMLInputElement>(null);

    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        const name = nameRef.current?.value || '';
        const age = ageRef.current?.value || '';
        alert(`名前: ${name}, 年齢: ${age}`);
    };

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label>名前:</label>
                <input
                    type="text"
                    defaultValue="太郎"
                    ref={nameRef}
                />
            </div>
            <div>
                <label>年齢:</label>
                <input
                    type="number"
                    defaultValue="20"
                    ref={ageRef}
                />
            </div>
            <button type="submit">送信</button>
        </form>
    );
};

React Hook Form

React Hook Form は、React アプリケーションにおけるフォームの実装と管理を効率化するためのライブラリ。React の フックベースの設計を活かして、フォームの入力値、バリデーション、エラーメッセージ、送信処理などを 最小限のコードでシンプルに扱えるのが特徴。

主な特徴

  • 再レンダリングを最小限に抑えることでパフォーマンスが高い
  • バリデーション機能が豊富(独自ルール+外部ライブラリとの統合も可)
  • 型安全な開発が可能(TypeScript との親和性が高い)

なぜ必要なのか

React でフォームを実装する際、useState を使って入力値を一つひとつ管理すると、コードが煩雑になりやすく、パフォーマンスの低下や状態管理のミスにつながることがある。React Hook Form は、少ない記述で効率よくフォームを構築・管理できるライブラリであり、再レンダリングの最小化やバリデーションの簡素化を通じて、保守性・パフォーマンス・ユーザー体験の向上を実現する。

基本的な使い方

import { useForm } from 'react-hook-form';

type FormData = {
    name: string;
    age: number;
};

export const BasicForm = () => {
    const {
        register, // フィールド登録
        handleSubmit, // 送信処理
        formState: { errors }, // バリデーションエラー
    } = useForm<FormData>();

    const onSubmit = (data: FormData) => {
        alert(`名前: ${data.name}, 年齢: ${data.age}`);
    };

    return (
        <form onSubmit={handleSubmit(onSubmit)}>
            <div>
                <label>名前:</label>
                <input
                    {...register('name', { required: '名前は必須です' })}
                />
                <p>{errors.name?.message}</p>
            </div>
            
            <div>
                <label>年齢:</label>
                <input
                    type="number"
                    {...register('age', {
                        required: '年齢は必須です',
                        min: { value: 0, message: '0歳以上を入力してください' },
                    })}
                />
                <p>{errors.age?.message}</p>
            </div>
            
            <button type="submit">送信</button>
        </form>
    );
};

useForm()

React Hook Form の中心となるフック。フォームの状態管理、バリデーション、送信処理などを一括で扱えるようになる。

主な戻り値

  • register: 入力項目を登録する関数
  • handleSubmit: バリデーション後に onSubmit を実行するラッパー関数
  • formState.errors: 各フィールドのバリデーションエラー
const {
    register, // フィールド登録
    handleSubmit, // 送信処理
    formState: { errors }, // バリデーションエラー
} = useForm<FormData>();

register()

指定されたフィールドに対応するイベントハンドラー、参照などを登録する関数。検証ルールもここで同時に指定できる。

<input
    {...register('name', { required: '名前は必須です' })}
/>

register() の戻り値は onChangeonBlurrefname からなるオブジェクトである。スプレッド構文を使うことで以下と等価のコードになる。

<input
    onChange={onChange}
    onBlur={onBlur}
    ref={ref}
    name={name}
/>

handleSubmit()

フォームの送信イベントを処理するためのラッパー関数。バリデーションを実行し、成功した場合にのみコールバック(onSubmit)を実行する。

<form onSubmit={handleSubmit(onSubmit)}>

errors

各フィールドのバリデーションエラーを保持するオブジェクト。errors.<フィールド名> にアクセスすることで、エラーメッセージなどを取得できる。

<p>{errors.name?.message}</p>

独自検証ルール

register では、組み込み以外に独自の検証関数も渡せる。validate に関数を渡し、条件に合わなければエラーメッセージを返すことで自由なバリデーションが可能。

<input
    {...register('age', {
        validate: value => 
            value >= 18 || '18歳以上である必要があります'
    })}
/>

フォームの状態管理

フォームの状態を formState オブジェクトで管理している。主に以下のような状態を扱う。

プロパティ名 内容
isDirty 入力値が初期値から変更されたかどうか
isValid 全フィールドのバリデーションが成功しているかどうか
isSubmitting フォーム送信中かどうか
isSubmitted 送信処理が完了したかどうか

これを利用して、送信ボタンの活性/非活性制御や、送信中のローディング表示などができる。

const {
    register,
    handleSubmit,
    formState: { errors, isDirty, isValid, isSubmitting },
} = useForm<FormData>({
    mode: 'onChange', // 入力値変更時にバリデーションを実行し、isValidを更新
});

return (
    <form onSubmit={handleSubmit(onSubmit)}>
        <button type="submit" disabled={!isDirty || !isValid || isSubmitting}>
            {isSubmitting ? '送信中...' : '送信'}
        </button>
    </form>
);

検証ライブラリとの連携

React Hook Form では簡単なバリデーションルールを記述できるが、複雑なルールやネストされた構造の検証には限界がある。そのため、外部のスキーマベースの検証ライブラリと組み合わせるのが一般的。

ライブラリ 特徴
Yup 最も一般的。直感的で柔軟なスキーマ記述が可能
Zod TypeScript に強く、スキーマと型の一貫性が高い
Joi Node.js 向けに開発されたが、React でも利用可
Valibot 軽量・高速で Zod 代替に注目されつつある
Yup を使ったコード例
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';

type FormData = {
    name: string;
    age: number;
};

// Yup スキーマ定義
const schema = Yup.object().shape({
    name: Yup.string()
        .required('名前は必須です')
        .min(2, '名前は2文字以上で入力してください'),
    age: Yup.number()
        .required('年齢は必須です')
        .min(18, '18歳以上である必要があります')
        .max(120, '120歳以下で入力してください'),
});

export const YupForm = () => {
    const {
        register,
        handleSubmit,
        formState: { errors, isDirty, isValid, isSubmitting },
    } = useForm<FormData>({
        resolver: yupResolver(schema), // Yup スキーマを適用
        mode: 'onChange', // 入力変更時にバリデーション実行
    });

    const onSubmit = (data: FormData) => {
        alert(`名前: ${data.name}, 年齢: ${data.age}`);
    };

    return (
        <form onSubmit={handleSubmit(onSubmit)}>
            <div>
                <label>名前:</label>
                <input {...register('name')} />
                <p>{errors.name?.message}</p>
            </div>

            <div>
                <label>年齢:</label>
                <input type="number" {...register('age')} />
                <p>{errors.age?.message}</p>
            </div>
    
            <button type="submit" disabled={!isDirty || !isValid || isSubmitting}>
                {isSubmitting ? '送信中...' : '送信'}
            </button>
        </form>
    );
};
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?