React Hook Formのチェックボックスの扱いに少しだけ詳しくなる

2024/11/30に公開

はじめに

React Hook Formでチェックボックスを扱う際、思わぬ挙動に悩まされることがあります。

この記事では、HTMLのチェックボックスの基本的な仕様から、React Hook Formでの実装方法まで、実際のコードを交えながら解説していきます。

チェックボックスの基本的な仕様

HTMLのチェックボックス要素とは

  • <input type="checkbox">として実装される、ユーザーが値をオン/オフできる入力要素
  • 主な属性として以下があります
    • name
      • フォームの要素を識別するための名前
      • 複数のチェックボックスで同じnameを共有することで、グループ化が可能
    • value
      • チェックボックスがチェックされた時にサーバーに送信される値
      • 指定しない場合は"on"が使用される
    • checked
      • 現在のチェック状態を表す論理属性
      • ユーザーの操作によって動的に変更される
      • フォーム送信時は、チェックされている場合のみ値が送信される
    • defaultChecked
      • 初期値(ページ読み込み時)のチェック状態を表す属性
      • 一度ユーザーが操作すると、この値は影響しなくなる

参考:MDN - input type="checkbox"

チェックボックスの状態

チェックボックスには以下の3つの状態があります:

  1. チェック済み(checked)
    • checked属性がtrueの状態
    • フォーム送信時にvalueの値が送信される
  2. 未チェック(unchecked)
    • checked属性がfalseの状態
    • フォーム送信時に値は一切送信されない
  3. 未決定(indeterminate)
    • JavaScriptのindeterminateプロパティでのみ設定可能
    • 複数の選択肢を統括するチェックボックスで使用される
    • フォーム送信時は未チェックと同様に扱われる

参考:MDN - input type="checkbox"

React Hook Formでのチェックボックスの挙動を見てみる

以下のサンプルコードを通じて、React Hook Formでのチェックボックスを扱った際の挙動を見ていきましょう。

ここでは、useFormのdefaultValuesによる初期値の設定を行い、チェックボックスの中身をconsole.logで出力します。

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

// フォームの型
type FormValues = {
  items: string[];
};

// フォームの実装
function App() {
  const { register, handleSubmit, watch } = useForm<FormValues>({
    defaultValues: { // 初期値の設定
      items: ['りんご', 'みかん'],
    },
  });

  // ref callbackが呼ばれ、checkedが更新されたことを確認するためのカスタムref
  const customRef = (element: HTMLInputElement | null) => {
    if (element) {
      console.log('--- ref callback が呼ばれました ---');
      console.log('element value:', element.value);
      console.log('element defaultChecked:', element.defaultChecked);
      console.log('element checked:', element.checked);
      console.log('------------------------');
    }
  };

  // watchを使って、フォームの値を監視
  const itemsValue = watch('items');

  // React Hook Form管理のチェックボックスの監視
  React.useEffect(() => {
    const controlledCheckboxes = document.querySelectorAll('input[name="items"]') as NodeListOf<HTMLInputElement>;

    controlledCheckboxes.forEach((checkbox) => {
      console.log('---チェックボックスの状態 ---');
      console.log('value:', checkbox.value);
      console.log('defaultChecked:', checkbox.defaultChecked);
      console.log('checked:', checkbox.checked);
      console.log('------------------------');
    });
  }, [itemsValue]);


  // フォーム送信時の処理
  const onSubmit = (data: FormValues) => {
    console.log('data:', data);
  };

  // フォームの項目
  const items = [
    {
      label: 'りんご',
      value: 'りんご',
    },
    {
      label: 'みかん',
      value: 'みかん',
    },
  ];

  return (
    <div style={{ padding: '20px' }}>
      <form onSubmit={handleSubmit(onSubmit)}>
        <br />
        {items.map((item) => (
          <label key={item.value}>
            <input
              type="checkbox"
              value={item.value}
              {...register('items')}
              ref={(element) => { // customrefを使う関係で別で上書きする
                register('items').ref(element);
                customRef(element);
              }}
            />
            {item.label}
            <br />
          </label>
        ))}
        <input type="submit" value="送信" />
      </form>
    </div>
  );
}

export default App;

内部の動作を調べる

初期表示時の挙動

最初の画面表示時、defaultValuesで指定した値(['りんご', 'みかん'])に基づいて、各チェックボックスの状態が設定されます。

画像を見ると、

  • register関数のref callbackが呼ばれ、checkedが更新されている
  • その時、以下のように値が設定されている
    • defaultChecked: false(指定していないため)
    • checked: true(defaultValuesで指定した項目)

ことがわかります。

register関数内のcheckboxの実装を追っていくと、以下のような処理が行われていることがわかります(間違っていたらすみません):

  1. refを通じてチェックボックスの要素情報を取得
  2. defaultValuesの値と照合
  3. 一致する項目のcheckedtrueに設定

3のcheckedの更新の具体的な実装は以下の通りです:

react-hook-form/src/logic/createFormControl.ts
// 省略
if (isCheckBoxInput(fieldReference.ref)) {
            fieldReference.refs.length > 1
              ? fieldReference.refs.forEach(
                  (checkboxRef) =>
                    // defaultCheckedがfalseで、disabledでないものをフィルター
                    (!checkboxRef.defaultChecked || !checkboxRef.disabled) &&
                    // checkedの更新
                    (checkboxRef.checked = Array.isArray(fieldValue)
                      ? !!(fieldValue as []).find(
                          (data: string) => data === checkboxRef.value,
                        )
                      : fieldValue === checkboxRef.value),
                )
              : fieldReference.refs[0] &&
                (fieldReference.refs[0].checked = !!fieldValue);
          } else {
            fieldReference.refs.forEach(
              (radioRef: HTMLInputElement) =>
                (radioRef.checked = radioRef.value === fieldValue),
            );
          }
// 省略

チェックボックス操作時の挙動

続いて、"りんご"のチェックボックスを押下した時の挙動を見ていきましょう。

画像を見ると、

  • りんごのチェックボックスがcheckedfalseに更新されている
    • その時、defaultCheckedfalseのまま

ということがわかります。

ユーザーがチェックボックスをクリックすると、React Hook Formは以下の流れで値を更新しています:

  1. ユーザーの操作により、チェックボックスのchecked状態が変化
  2. register関数内で設定されたonChangeイベントハンドラーが実行
  3. checkedtrueのチェックボックスをフィルタリングし、その値でフォームを更新

3のフィルタリングの具体的な実装は以下の通りです:

react-hook-form/src/logic/getCheckboxValue.ts
// 省略
export default (options?: HTMLInputElement[]): CheckboxFieldResult => {
  if (Array.isArray(options)) {
    if (options.length > 1) {
      const values = options
        // checkedがtrueで、disabledでないものをフィルター
        .filter((option) => option && option.checked && !option.disabled)
        .map((option) => option.value);
      return { value: values, isValid: !!values.length };
    }

    return options[0].checked && !options[0].disabled
      ? // @ts-expect-error expected to work in the browser
        options[0].attributes && !isUndefined(options[0].attributes.value)
        ? isUndefined(options[0].value) || options[0].value === ''
          ? validResult
          : { value: options[0].value, isValid: true }
        : validResult
      : defaultResult;
  }

  return defaultResult;
};
// 省略

このように、React Hook Formは内部でチェックボックスの状態を適切に管理し、フォームの値として反映してくれます。

まとめ

この記事では、以下の内容について解説しました:

  1. HTMLのチェックボックスの基本的な仕様

    • checkeddefaultCheckedvalueなどの重要な属性の役割
    • チェックボックスの3つの状態(checked、unchecked、indeterminate)
  2. React Hook Formでのチェックボックスの実装

    • defaultValuesによる初期値の設定
    • register関数による状態管理
    • 内部実装における値の更新フロー

この記事が何かの役に立てば幸いです!

Discussion