React Hook Formのチェックボックスの扱いに少しだけ詳しくなる
はじめに
React Hook Formでチェックボックスを扱う際、思わぬ挙動に悩まされることがあります。
この記事では、HTMLのチェックボックスの基本的な仕様から、React Hook Formでの実装方法まで、実際のコードを交えながら解説していきます。
チェックボックスの基本的な仕様
HTMLのチェックボックス要素とは
-
<input type="checkbox">
として実装される、ユーザーが値をオン/オフできる入力要素 - 主な属性として以下があります
-
name
- フォームの要素を識別するための名前
- 複数のチェックボックスで同じ
name
を共有することで、グループ化が可能
-
value
- チェックボックスがチェックされた時にサーバーに送信される値
- 指定しない場合は
"on"
が使用される
-
checked
- 現在のチェック状態を表す論理属性
- ユーザーの操作によって動的に変更される
- フォーム送信時は、チェックされている場合のみ値が送信される
-
defaultChecked
- 初期値(ページ読み込み時)のチェック状態を表す属性
- 一度ユーザーが操作すると、この値は影響しなくなる
-
参考:MDN - input type="checkbox"
チェックボックスの状態
チェックボックスには以下の3つの状態があります:
- チェック済み(checked)
-
checked
属性がtrue
の状態 - フォーム送信時に
value
の値が送信される
-
- 未チェック(unchecked)
-
checked
属性がfalse
の状態 - フォーム送信時に値は一切送信されない
-
- 未決定(indeterminate)
- JavaScriptの
indeterminate
プロパティでのみ設定可能 - 複数の選択肢を統括するチェックボックスで使用される
- フォーム送信時は未チェックと同様に扱われる
- JavaScriptの
参考: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の実装を追っていくと、以下のような処理が行われていることがわかります(間違っていたらすみません):
-
ref
を通じてチェックボックスの要素情報を取得 -
defaultValues
の値と照合 - 一致する項目の
checked
をtrue
に設定
3のchecked
の更新の具体的な実装は以下の通りです:
// 省略
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),
);
}
// 省略
チェックボックス操作時の挙動
続いて、"りんご"のチェックボックスを押下した時の挙動を見ていきましょう。
画像を見ると、
- りんごのチェックボックスが
checked
がfalse
に更新されている- その時、
defaultChecked
はfalse
のまま
- その時、
ということがわかります。
ユーザーがチェックボックスをクリックすると、React Hook Formは以下の流れで値を更新しています:
- ユーザーの操作により、チェックボックスの
checked
状態が変化 -
register
関数内で設定されたonChange
イベントハンドラーが実行 -
checked
がtrue
のチェックボックスをフィルタリングし、その値でフォームを更新
3のフィルタリングの具体的な実装は以下の通りです:
// 省略
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は内部でチェックボックスの状態を適切に管理し、フォームの値として反映してくれます。
まとめ
この記事では、以下の内容について解説しました:
-
HTMLのチェックボックスの基本的な仕様
-
checked
、defaultChecked
、value
などの重要な属性の役割 - チェックボックスの3つの状態(checked、unchecked、indeterminate)
-
-
React Hook Formでのチェックボックスの実装
-
defaultValues
による初期値の設定 -
register
関数による状態管理 - 内部実装における値の更新フロー
-
この記事が何かの役に立てば幸いです!
Discussion