React Hook Form を使って、チェックボックスで1つ以上のチェックを必須とするフォームを作ってみよう

React Hook Form を使って、チェックボックスで1つ以上のチェックを必須とするフォームを作ってみよう|ANDPAD Advent Calendar 2022

この記事は ANDPAD Advent Calendar 2022 の 22日目の記事です。

はじめまして、私フロントエンドエンジニアのはつし(蓮子)と申します。盟友&尊敬するマネージャーの櫻井賢司こと KJ からバトンを受け取り、22日目を担当させていただきます。slack上のニックネームは 824 で、メンションつける時は1秒で済むのがメリットです。あだ名を聞かれたら一応「はっちゃん」と呼ばれてましたと言うようにしていますが、ちゃん付けがとても似合わないスキンヘッド&髭面なので、ヒゲおじさんとかでもOKです。

最近の趣味は、オフィスのある秋葉原で、路地裏の美味しいの食事処を探すことです。仲間募集中。

アンドパッドに2021年12月に入社して、ちょうど1年が経とうとしたタイミングでブログを書くことになったのは何かの縁なのかもしれません。そんな記念すべき日に何を書こうか悩みましたが、まずはTips的な内容でジャブを打っていこうと思います。

経歴的に、 Nuxt.js を使った開発期間が長く、アンドパッドでも最初は Nuxt.js でプロダクトを開発していたのですが、先日初めて Next.js で開発する機会をいただきました。

色々と勝手が違って四苦八苦しながら頑張っているのですが、個人的に最初にハマった form で複数チェックボックスのバリデーションを実装した時に調べたことを書いていこうかなと思います。ググっても意外にヒットしなかったので、世界中の誰かのほんの少しでも役に立てたら光栄です(全部日本語)

今回作りたいもの

いきなりですが、今回のゴールのビジュアルはこちらです。1つ以上のチェックボックスで、指定した key(サンプルなので myFavoriteFoods にしました) でチェックしたデータを送信します。1つ以上チェックが入っているかどうかバリデーションチェックして、送信されるデータが正規なものになるようにします。

環境

  • node 18.12.0
  • Next.js 13.0.0
  • react-hook-form 7.41.0

仕様

  • 1つ以上の選択を必須にする
  • onChange をフックにバリデーションチェックする
  • エラー時、ボタンを disable にし、エラーテキストを出す
  • submit 時の データ形式は { myFavoriteFoods: ['食べ物1', '食べ物2', '食べ物3'] }

1. 静的にフォームをコーディング

とりあえずあまり深く考えずに、下記のようにベースをコーディングし、肉付けしていきます。CSSは割愛します。

types/index.d.ts

interface MyFavoriteFoods {
  id: string;
  name: string;
  checked: boolean;
  disabled: boolean;
}

pages/index.tsx

import type { NextPage } from "next";
import { useState } from "react";

const MultipleCheckbox: NextPage = () => {
  const [myFavoriteFoods, setMyFavoriteFoods] = useState<MyFavoriteFoods[]>([
    {
      id: "sushi",
      name: "寿司",
      checked: false,
      disabled: false,
    },
    {
      id: "yakiniku",
      name: "焼肉",
      checked: false,
      disabled: false,
    },
    {
      id: "khao_mangai",
      name: "カオマンガイ",
      checked: false,
      disabled: false,
    },
  ]);

  return (
    <div>
      <form>
        <div>
          {myFavoriteFoods.map((item) => {
            return (
              <div key={item.id}>
                <input
                  id={item.id}
                  type="checkbox"
                  defaultChecked={item.checked}
                  disabled={item.disabled}
                />
                <label htmlFor={item.id}>{item.name}</label>
              </div>
            );
          })}
        </div>
        <button type="submit">確定</button>
      </form>
    </div>
  );
};

export default MultipleCheckbox;

2. React Hook Form を使う

おそらく React でフォーム管理するなら React Hook Form が代表的だと思います。 React Hook Form から下記 module を import します。

  • useForm(formのフック)
  • SubmitHandler(onSubmit処理の返り値の型)

useForm からは下記を読み込んでおきます。

  • register(引数に input の name を指定することで、バリデーションなどのオプションを適用可能。送信時のデータの key になる)
  • handleSubmit(送信時のフックを受け取れる関数)
  • formState(formの状態管理オプション)
    • errors
    • isValid
    • isSubmitting

form 送信時のデータの型も作っておきましょう。

バリデーションチェックをonChangeのタイミングにするために、 useForm の引数に { mode: "onChange" } を指定します。

ボタンのdisabledプロパティに {!isValid || isSubmitting} と書くことで、バリデーションエラーか submit 中の時に true にすることができます。

types/index.d.ts

// 追記
interface SubmitData {
  myFavoriteFoods: string[];
}

pages/index.tsx

 // 追記
import { useForm, SubmitHandler } from "react-hook-form";
…

const MultipleCheckbox: NextPage = () => {
  // 追記
  const {
    register,
    handleSubmit,
    formState: { errors, isValid, isSubmitting },
  } = useForm<SubmitData>({ mode: "onChange" });

  // 追記
  const onSubmit: SubmitHandler<SubmitData> = (data: SubmitData) => { /* API送信 */ };
  …

  <form onSubmit={handleSubmit(onSubmit)}>  // 追記
    <div>
      {myFavoriteFoods.map((item) => {
        return (
          <div key={item.id}>
            <input
              id={item.id}
              type="checkbox"
              value={item.id}
              defaultChecked={item.checked}
              disabled={item.disabled}
              {...register("myFavoriteFoods")}  // 追記
            />
            <label htmlFor={item.id}>{item.name}</label>
          </div>
        );
      })}
    </div>
    <button type="submit" disabled={!isValid || isSubmitting}>確定</button>  // 追記
  </form>
  …

バリデーションを実装する

register の引数に validate オプションを記載すると、予め決められているバリデーションルールを追加したり、オリジナルのルールを作ることができます。

今回は、1つ以上のチェックを必須にする なので、 atLeastOneRequired という名前をつけました。複数のチェックボックスの場合、validate の引数の value には、input の value が array で入ってくるので、length を見てエラーを返すように設定します。

errors.myFavoriteFoods でエラーオブジェクトを取得することができるので、HTMLを追記します。

詳しくはこちらをご参照ください。

react-hook-form.com

pages/index.tsx

{...register("myFavoriteFoods", {
    // 追記
    validate: {
      atLeastOneRequired: (value) =>
        value.length >= 1) || "1つ以上選択してください",
    },
  })}// 追記
{errors.myFavoriteFoods && (
  <p>
    {errors.myFavoriteFoods.type}: {errors.myFavoriteFoods.message}
  </p>
)}

3. チェックボックスが1つしかない場合を考慮する

これは実装して初めてわかったのですが、checkboxが1つの時と複数の時で、送信される型が変わります。わかりやすいように、watch関数を読み込んで、送信するデータを可視化してみましょう。

pages/index.tsx

const {
    register,
    watch, // 追記
    handleSubmit,
    formState: { errors, isValid, isSubmitting },
  } = useForm<SubmitData>({ mode: "onChange" });

  // 追記
  const watchAllFields = watch();
…
  // コメントアウト
  /* {
    id: "sushi",
    name: "寿司",
    checked: false,
    disabled: false,
  },
  {
    id: "yakiniku",
    name: "焼肉",
    checked: false,
    disabled: false,
  }, */
  {
    id: "khao_mangai",
    name: "カオマンガイ",
    checked: false,
    disabled: false,
  },
…

  // 追記
  <p>送信するデータ: {JSON.stringify(watchAllFields)}</p>
…

すると、チェックした場合、value が string で保存され、未チェックの場合は boolean になりました。

APIにデータを送信する時、array と string 両方のパターンを考慮しておかないと、バグの温床になりそうです。

型に string を追加し、 value の boolean も判定に追加しておきましょう

types/index.d.ts

interface SubmitData {
  myFavoriteFoods: string | string[];
}

pages/index.tsx

atLeastOneRequired: (value) =>
  (value && value.length >= 1) || "1つ以上選択してください",
},

4. 完成

完成したコードをはこちらになります。

types/index.d.ts

interface MyFavoriteFoods {
  id: string;
  name: string;
  checked: boolean;
  disabled: boolean;
}

interface SubmitData {
  myFavoriteFoods: string | string[];
}

pages/index.tsx

import type { NextPage } from "next";
import { useState } from "react";
import { useForm, SubmitHandler } from "react-hook-form";

const MultipleCheckbox: NextPage = () => {
  const {
    register,
    watch,
    handleSubmit,
    formState: { errors, isValid, isSubmitting },
  } = useForm<SubmitData>({ mode: "onChange" });

  const watchAllFields = watch();

  const [myFavoriteFoods, setMyFavoriteFoods] = useState<MyFavoriteFoods[]>([
    {
      id: "sushi",
      name: "寿司",
      checked: false,
      disabled: false,
    },
    {
      id: "yakiniku",
      name: "焼肉",
      checked: false,
      disabled: false,
    },
    {
      id: "khao_mangai",
      name: "カオマンガイ",
      checked: false,
      disabled: false,
    },
  ]);

  const onSubmit: SubmitHandler<SubmitData> = (data: SubmitData) => {
    // API送信
  };

  return (
    <div>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <div>
            {myFavoriteFoods.map((item) => {
              return (
                <div key={item.id}>
                  <input
                    id={item.id}
                    type="checkbox"
                    value={item.id}
                    defaultChecked={item.checked}
                    disabled={item.disabled}
                    {...register("myFavoriteFoods", {
                      validate: {
                        atLeastOneRequired: (value) =>
                          (value && value.length >= 1) ||
                          "1つ以上選択してください",
                      },
                    })}
                  />
                  <label htmlFor={item.id}>{item.name}</label>
                </div>
              );
            })}
          </div>
          {errors.myFavoriteFoods && (
            <p>
              {errors.myFavoriteFoods.type}: {errors.myFavoriteFoods.message}
            </p>
          )}
        </div>
        <button type="submit" disabled={!isValid || isSubmitting}>確定</button>
        <p>送信するデータ: {JSON.stringify(watchAllFields)}</p>
      </form>
    </div>
  );
};

export default MultipleCheckbox;

個人的に register の概念を理解するのが難しかったです。公式に内容が記載されていますが、文字通りトリガーとなる関数やプロパティをまとめて「登録」できる便利なものなんですね。

<input 
  onChange={onChange} // assign onChange event 
  onBlur={onBlur} // assign onBlur event
  name={name} // assign name prop
  ref={ref} // assign ref prop
/>
// same as above
<input {...register('firstName')} />

キャプチャ元のコードは CodeSandbox に置いておきましたので、よかったら見てみてください!

codesandbox.io

おわりに

アンドパッドでは、Vue.js / Nuxt.js 、React.js / Next.js の採用実績があり、チャンスがあれば自分で環境を決めることができます。書籍購入補助制度があったり、勉強会や輪読会を自主的に開催し知見を深めたり、エンジニアにとってとても働きやすい環境です。そんなアンドパッドではエンジニアの積極採用中です。このような取り組みや開発環境に興味がありましたら、ぜひお気軽にカジュアル面談などご参加ください! 最後までお読みいただきありがとうございました!

hrmos.co

engineer.andpad.co.jp

明日は開発本部の明里さんからk8s基盤の記事を紹介する予定です。お楽しみに!