React + TypeScriptでチェックボックスコンポーネントを作る
フォームを作ると必ず出てくる問題
Reactでフォームを作っていると、こんな経験はありませんか。
「チェックボックスをあちこちで使うのに、毎回HTMLを書くのは面倒だ」「React Hook Formと連携させたいけど、refの渡し方がわからない」「ラベルをクリックしてもチェックが入らない」
本記事では、これらの問題をすべて解決する、再利用可能なチェックボックスコンポーネントを作ります。コードは30行以下です。
先に完成形を見る
まず、最終的に作るコンポーネントがどう動くかを見ます。
// 使い方
<Checkbox label="利用規約に同意する" />
<Checkbox label="通知を受け取る" disabled />
<Checkbox label="購読する" onChange={(e) => console.log(e.target.checked)} />
たった1行で、スタイル付き・アクセシビリティ対応・イベント処理可能なチェックボックスが使えます。
実装コードは次の通りです。
import { InputHTMLAttributes, forwardRef } from 'react'
interface CheckboxProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string
}
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
({ label, className = '', ...props }, ref) => {
return (
<label className="flex items-center gap-2 cursor-pointer">
<input
ref={ref}
type="checkbox"
className={`w-5 h-5 text-blue-500 rounded focus:ring-2 focus:ring-blue-500 ${className}`}
{...props}
/>
{label && <span className="text-sm text-gray-700">{label}</span>}
</label>
)
}
)
Checkbox.displayName = 'Checkbox'
このコードには、4つの重要な技術が含まれています。順番に解説します。
技術1: InputHTMLAttributesで標準属性を全部受け取る
interface CheckboxProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string
}
InputHTMLAttributes<HTMLInputElement>は、HTML標準のinput要素が持つ属性の型定義です。これを継承することで、次のような属性がすべて使えます。
disabled: 無効化checked: チェック状態onChange: 変更イベントname: フォーム送信時の名前value: 値
これにより、「あれ、この属性使えないの?」という問題が起きません。標準のinput要素でできることは、すべて自動的に使えます。
label?: stringは、チェックボックスの横に表示するテキストです。?は「省略可能」を意味します。
技術2: forwardRefでDOM要素への参照を渡す
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
({ label, className = '', ...props }, ref) => {
// 中身
}
)
forwardRefは、親コンポーネントから子コンポーネントのDOM要素に直接アクセスできるようにする機能です。
なぜこれが必要なのか。React Hook Formなどのフォームライブラリを使う場合、次のようなコードを書きます。
const { register } = useForm()
<Checkbox {...register('agreeToTerms')} />
このregisterが、内部でDOM要素への参照(ref)を必要とします。forwardRefを使わないと、このコードはエラーになります。
forwardRef<参照先の型, Propsの型>という形で、2つの型を指定します。
- 第一引数:
HTMLInputElement(input要素の型) - 第二引数:
CheckboxProps(コンポーネントのPropsの型)
関数の引数として、(props, ref)を受け取ります。propsは通常のPropsで、refは親から渡された参照です。
{ label, className = '', ...props }は、分割代入です。labelとclassNameを取り出し、残りをpropsとしてまとめます。
技術3: labelでクリック範囲を広げる
<label className="flex items-center gap-2 cursor-pointer">
<input ... />
{label && <span>{label}</span>}
</label>
input要素をlabelで囲むと、テキスト部分をクリックしてもチェックボックスが反応します。これは、アクセシビリティの基本です。
className="flex items-center gap-2 cursor-pointer"は、Tailwind CSSのクラスです。
flex: 横並び配置items-center: 縦方向の中央揃えgap-2: 要素間の余白cursor-pointer: マウスカーソルを手の形にする
{label && <span>{label}</span>}は、条件付きレンダリングです。labelが渡されている場合のみ、span要素を表示します。JavaScriptの&&演算子は、左側が真なら右側を評価します。
技術4: スプレッド構文で属性を展開する
<input
ref={ref}
type="checkbox"
className={`w-5 h-5 text-blue-500 rounded focus:ring-2 focus:ring-blue-500 ${className}`}
{...props}
/>
{...props}は、残りのすべてのPropsをinput要素に展開します。これにより、次のような使い方がすべて動作します。
<Checkbox disabled />
<Checkbox checked onChange={handleChange} />
<Checkbox name="newsletter" value="subscribe" />
classNameでは、テンプレートリテラルを使って、デフォルトのスタイルと外部から渡されたスタイルを結合しています。
`デフォルトのクラス ${追加のクラス}`
これにより、基本的な見た目を維持しながら、必要に応じてカスタマイズできます。
displayNameでデバッグしやすくする
Checkbox.displayName = 'Checkbox'
forwardRefを使うと、React DevToolsで「ForwardRef」と表示されます。displayNameを設定することで、「Checkbox」と表示されるようになります。
開発中にコンポーネントを特定しやすくなるため、必ず設定してください。
実際に使ってみる
import { useState } from 'react'
import { Checkbox } from './Checkbox'
function App() {
const [agreed, setAgreed] = useState(false)
return (
<div>
<Checkbox
label="利用規約に同意する"
checked={agreed}
onChange={(e) => setAgreed(e.target.checked)}
/>
<button disabled={!agreed}>登録する</button>
</div>
)
}
このコードでは、チェックボックスの状態をuseStateで管理し、同意しないとボタンが押せないようにしています。
React Hook Formと連携させる場合は、次のようになります。
import { useForm } from 'react-hook-form'
import { Checkbox } from './Checkbox'
function App() {
const { register, handleSubmit } = useForm()
const onSubmit = (data) => {
console.log(data) // { agreeToTerms: true }
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Checkbox
label="利用規約に同意する"
{...register('agreeToTerms')}
/>
<button type="submit">登録する</button>
</form>
)
}
{...register('agreeToTerms')}が、自動的にrefやonChangeなどを設定します。forwardRefを使っているため、これが正常に動作します。
まとめ
本記事では、4つの技術を使ってチェックボックスコンポーネントを作成しました。
InputHTMLAttributesで標準属性をすべて使えるようにするforwardRefで外部からDOM要素にアクセスできるようにするlabel要素でクリック範囲を広げる- スプレッド構文で属性を柔軟に渡せるようにする
このコンポーネントは、フォームライブラリとの連携やカスタムスタイルの追加など、様々な場面で使用できます。