📝

React Hook Formで動的フォームを作ってみた

2022/12/11に公開
2

今回は、React Hook Formで動的フォームを作る方法について共有します!
今年に入るまで、業務でもReact Hook Formを扱ったことはなかったのですが、4月から参画しているプロジェクトがきっかけで、プライベートでも使用してみることにしました。

通常のフォーム作成だけでも良かったのですが、フィールドの数が増減する「動的フォーム」の実装に憧れて、今回挑戦してみました🔥

※ところどころany型で逃げてるのは許してね!!12月だし、年末近いし...笑

そもそもReact Hook Formとは

React Hook Formとは、Reactでフォームを簡単に扱えるライブラリです。
入力値の取得やバリデーションなどを行うことができます。

本記事では、そんなReact Hook Formの基本の扱い方から動的フォームの作り方までお伝えできたら良いなと思っています!

通常のフォームの作り方

まずは、通常のフォームの作り方から見ていきます。

以下の画像のような、入力フィールドが1つだけ存在するシンプルなフォームを作ってみます。

先にこのフォームを実装したコード全体を見せます。(import周りは端折ってます。)

const SampleForm = () => {
    const { register, handleSubmit, watch, reset, formState: { errors } } = useForm();

    const watchAllVal = watch(); // 入力値を監視

    // フォーム送信
    const submitForm = (data: any): void => {
        console.log(data);

        // フォームを空にする。
        reset();
    };

    return (
        <div>
            <h1>基本のフォーム</h1>
            <form>
                <div>
                    <label>title:</label>
                    <input {...register('title', { pattern: /[A-Za-z]/ })} />
                    {errors.title && <span>文字を入力してください!</span>}
                </div>
                <input type='submit' onClick={handleSubmit(submitForm)} />
            </form>

            <div>
                {/* 現在入力されている値 */}
                <p>title:{watchAllVal.title}</p>
            </div>
        </div>
    );
};
export default SampleForm;

JSX部分について

まず、JSX部分に注目してみます。

    return (
        <div>
            <h1>基本のフォーム</h1>
            <form>
                <div>
                    <label>title:</label>
                    <input {...register('title', { pattern: /[A-Za-z]/ })} />
                    {errors.title && <span>アルファベットを入力してください!</span>}
                </div>
                <input type='submit' onClick={handleSubmit(submitForm)} />
            </form>

            <div>
                {/* 現在入力されている値 */}
                <p>title:{watchAllVal.title}</p>
            </div>
        </div>
    );

そもそも<input>要素を設置することで値の入力をすること自体はできます。
ただ、値の入力・取得を行えるようにするには、register関数を使う必要があります。
このregister関数を実行することで、フォームの変更に応じてevent.target.valueなどで値を取得できるonChangeイベントやname属性の設定などが行われています。
公式ドキュメントのregisterを参照

...register(フィールドのname)というように使います。(form関係の要素に、name属性がありますよね。)
また、第2引数に正規表現(pattern)や、最大文字数(maxLength)等をオブジェクトとして渡すことで、バリデーションを行うことができます。
今回の例では、アルファベット以外が入力されると、エラーメッセージが表示されるようにしています。

<input {...register('title', { pattern: /[A-Za-z]/ })} />
{errors.title && <span>アルファベットを入力してください!</span>}

試しに、アルファベットではなく数字を入れてみました。
エラーメッセージ(「アルファベットを入力してください!」)が表示されています。

watch関数を使うことで入力値を全て監視することができます。

const watchAllVal = watch(); // 入力値を監視

〜中略〜

{/* 現在入力されている値 */}
<p>title:{watchAllVal.title}</p>

入力値はオブジェクトになっており、watchAllVal.titleというように、フィールドのnameをキー名として指定して、現在の入力状況を取得することが可能です。画像だと伝わりにくいと思うので、機会があれば試してみてください🙇‍♀️

<input>要素だけでなく、<select>要素も同じように取り扱うことができます。

    return (
        <div>
            <h1>基本のフォーム</h1>
            <form>
                <div>
                    <label>title:</label>
                    <input {...register('title', { pattern: /[A-Za-z]/ })} />
                    {errors.title && <span>アルファベットを入力してください!</span>}
                </div>

                {/* select要素 */}
                <div>
                    <label>ラベル:</label>
                    <select {...register('select')}>
                        <option defaultChecked>選択してください</option>
                        <option value='aaa'>aaa</option>
                        <option value='bbb'>bbb</option>
                        <option value='ccc'>ccc</option>
                    </select>
                    {errors.select && <span>どれか1つ以上チェックを入れてください!</span>}
                </div>
                <input type='submit' onClick={handleSubmit(submitForm)} />
            </form>

            <div>
                {/* 現在入力されている値 */}
                <p>title:{watchAllVal.title}</p>
                <p>select:{watchAllVal.select}</p>
            </div>
        </div>
    );

基本のフォーム送信について

それでは、フォーム送信部分について見ていきます。

フォーム送信は、JSXで見るとこちらで実行されていました。

<input type='submit' onClick={handleSubmit(submitForm)} />

handleSubmit関数でバリデーションを実行し、submitForm関数(関数名は任意です。)が入力値を受け取って実行されます。
私が用意したsubmitForm関数は、以下のような内容になっています。

// フォーム送信
const submitForm = (data: any): void => {
    console.log(data);

    // フィールドを空にする。
    reset();
};

reset関数を使うことで、フォーム送信を行なった後、フィールドを空にすることができます。
引数として渡ってきた入力値を使って、API実行してデータベースに追加する...みたいな使い方もできますね!

動的フォームの作り方

React Hook Formの基本的な扱い方については、わかって頂けたかなと思います。
ここからは、本題である、動的フォームについてお伝えしていきたいと思います。

以下の画像のような、入力フィールドを先頭や後に追加できるような動的フォームを作ってみます。

先にこのフォームを実装したコード全体を見せます。(import周りは端折ってます。)

type AddTasksParamType = {
    sample: string;
};

const SampleDynamicForm = () => {
    const { register, handleSubmit, control, reset } = useForm({
        defaultValues: {
          sample: [
            {
                title: ''
            }
          ] 
        }
    });
    const { fields, prepend, append, remove } = useFieldArray({
        control,
        name: 'sample'
    });
    
    const onSubmit = (data: any): void => {
        console.log(data);

        // フォームを空にする。
        reset();
    };

    return (
        <div>
            <h1>動的フォーム</h1>
            <form>
                <button type="button" onClick={() => prepend({title: ''})}>
                    <FontAwesomeIcon icon={faPlus} />先頭に追加
                </button>
                {fields.map((field: any, index: number) => (
                    <div key={field.id}>
                        <label>タスクNo.{index}</label>
                        <div>
                            <input {...register(`sample.${index}.title`)} placeholder='ここにタスク名を入力してください' />
                            <FontAwesomeIcon icon={faCircleXmark} className='removeFormIcon' onClick={() => remove(index)} />
                        </div>
                    </div>
                ))}
                <button type="button" onClick={() => append({title: ''})}>
                    <FontAwesomeIcon icon={faPlus} />後ろに追加
                </button>

                <div>
                    <BackToHomeButton />
                    <SubmitButton onClick={handleSubmit(onSubmit)} text='登録' />
                </div>
            </form>
        </div>
    );
};

export default SampleDynamicForm;

動的フォームを作成するためには、React Hook Formで提供されているuseFieldArrayフックを利用します。

const { fields, prepend, append, remove } = useFieldArray({
    control,
    name: 'sample'
});

fieldsには、デフォルト値が入ったオブジェクトを要素する配列になっています。
入力フィールドが増減することで、この配列の要素も増減します。
useFieldArrayフックに渡すオブジェクトの中に、nameプロパティがありますが、これは、入力値を格納するオブジェクトのキー名となります。

JSX部分について

ここでも、JSX部分に着目してみます。

return (
    <div>
        <h1>動的フォーム</h1>
        <form>
            <button type="button" onClick={() => prepend({title: ''})}>
                <FontAwesomeIcon icon={faPlus} />先頭に追加
            </button>

            {/* 配列fieldsを展開する */}
            {fields.map((field: any, index: number) => (
                <div key={field.id}>
                    <label>タスクNo.{index}</label>
                    <div>
                        <input {...register(`sample.${index}.title`)} placeholder='ここにタスク名を入力してください' />
                        <FontAwesomeIcon icon={faCircleXmark} className='removeFormIcon' onClick={() => remove(index)} />
                    </div>
                </div>
            ))}


            <button type="button" onClick={() => append({title: ''})}>
                <FontAwesomeIcon icon={faPlus} />後ろに追加
            </button>

            <ButtonsArea>
                <BackToHomeButton />
                <SubmitButton onClick={handleSubmit(onSubmit)} text='登録' />
            </ButtonsArea>
        </form>
    </div>
);

入力フィールドが要素となっている配列fieldsmap関数で展開していくことで、入力フィールドを表示しています。

入力フィールドを追加〜append/prepend〜

それでは、入力フィールドの増減について見ていきます。

まずはよくある、後ろに追加するパターンです。
append関数を使用します。

<button type="button" onClick={() => append({title: ''})}>
    <FontAwesomeIcon icon={faPlus} />後ろに追加
</button>

append関数の第1引数に、追加するフィールドの状態をオブジェクトとして渡します。ここでは、空の状態で追加したいので、空のままで渡しています。
ここでもし、空文字ではなく何か値を入れた状態で渡すと、その値が既に入力されたフィールドが追加されます。
append関数が実行されることで、配列fieldsの要素も増加します。そのため、配列fieldsが展開する要素が増えるので、表示される入力フィールドも増えていきます。

また、先頭に追加することもできます。
prepend関数を使用します。

<button type="button" onClick={() => prepend({title: ''})}>
    <FontAwesomeIcon icon={faPlus} />先頭に追加
</button>

使う関数が、prepend関数になったくらいで、構文的にはappend関数と変わりありません。
もちろん、prepend関数が実行されることで、配列fieldsの要素も増加します。

どちらを使用するかは、フィールドを「どこに追加したいか」によって異なってきます。

フォーム送信について

フォーム送信については、基本のフォーム送信と変わらないので、前出の基本のフォーム送信についてをご参照下さい。

ちなみに、動的フォームでフォーム送信で受け取る入力値は、以下のような形式になっています。

useFieldArrayフックのnameで指定したsampleをキー名とするオブジェクトが取得できます。フィールドの入力値は、配列の要素となっています。

感想

フォームを扱うことは業務でも多々あるかと思います。
React Hook Formを使うことで、入力した値の取得などが簡単に行えるようになります。
動的フォームもuseFieldArrayフックを使用することで簡単に作成することができてしまいます。
「車輪の再発明をしたい!」という気分の時以外には、使うと便利だな〜と思います!

認識の誤り・補足などがあれば、是非、コメントして頂けますと助かります〜!

長文お読み下さり、ありがとうございました!

参考資料

React Hook Form公式ドキュメント

GitHubで編集を提案

Discussion

nap5nap5

記事の内容を生かしつつデモ作ってみました。

https://codesandbox.io/p/sandbox/determined-cray-22kzjf?file=%2FREADME.md&selection=[{"endColumn"%3A1%2C"endLineNumber"%3A3%2C"startColumn"%3A1%2C"startLineNumber"%3A3}]

/todoページがデモページになります。

簡単ですが、以上です。

あずにゃんあずにゃん

nap5さん

本記事を参考にして下さり、ありがとうございます🙇‍♀️
お役に立てたのであれば、とても嬉しいです!!