概要
useState で値を React が管理する「制御コンポーネント」と、useRef で DOM を直接参照する「非制御コンポーネント」の違いを実装例で比較する。それぞれの動作の違いと、どちらを選ぶべきかの判断基準を示す。
インストール
# 追加インストールは不要
実装
制御コンポーネント(Controlled)
// components/ControlledForm.tsx
"use client";
import { useState } from "react";
export function ControlledForm() {
const [name, setName] = useState("");
const [submitted, setSubmitted] = useState<string | null>(null);
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setSubmitted(name);
}
return (
<form onSubmit={handleSubmit} className="space-y-3">
<div>
<label className="block text-sm font-medium text-gray-700">名前</label>
{/* value を state で管理しているため、入力のたびに再レンダリングされる */}
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="mt-1 block w-full rounded border px-3 py-2 text-sm"
/>
{/* リアルタイムで入力値を参照できる */}
<p className="mt-1 text-xs text-gray-500">入力中: {name}</p>
</div>
<button
type="submit"
disabled={name.trim() === ""}
className="rounded bg-blue-600 px-4 py-2 text-sm text-white disabled:opacity-50"
>
送信
</button>
{submitted && (
<p className="text-sm text-green-600">送信されました: {submitted}</p>
)}
</form>
);
}
非制御コンポーネント(Uncontrolled)
// components/UncontrolledForm.tsx
"use client";
import { useRef, useState } from "react";
export function UncontrolledForm() {
const nameRef = useRef<HTMLInputElement>(null);
const [submitted, setSubmitted] = useState<string | null>(null);
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
// 送信時にのみ DOM から値を取得する
const value = nameRef.current?.value ?? "";
if (value.trim()) setSubmitted(value);
}
function handleReset() {
// DOM を直接操作してリセット
if (nameRef.current) nameRef.current.value = "";
setSubmitted(null);
}
return (
<form onSubmit={handleSubmit} className="space-y-3">
<div>
<label className="block text-sm font-medium text-gray-700">名前</label>
{/* defaultValue で初期値のみ設定。以降は React が管理しない */}
<input
type="text"
defaultValue=""
ref={nameRef}
className="mt-1 block w-full rounded border px-3 py-2 text-sm"
/>
{/* 入力中の値はリアルタイムに参照できない */}
</div>
<div className="flex gap-2">
<button
type="submit"
className="rounded bg-blue-600 px-4 py-2 text-sm text-white"
>
送信
</button>
<button
type="button"
onClick={handleReset}
className="rounded border px-4 py-2 text-sm text-gray-700"
>
リセット
</button>
</div>
{submitted && (
<p className="text-sm text-green-600">送信されました: {submitted}</p>
)}
</form>
);
}
比較表
| 観点 | 制御コンポーネント | 非制御コンポーネント |
|---|---|---|
| 値の管理 | React state(useState) | DOM(useRef) |
| 入力中の値参照 | できる(即時) | できない(取得時のみ) |
| バリデーション | 入力のたびに実行できる | 送信時に実行 |
| 再レンダリング | 入力のたびに発生 | 発生しない |
| 向いている用途 | 入力に応じて UI を変える場面 | シンプルな送信フォーム |
ポイント
- 制御コンポーネントは
value+onChangeで React が値を所有するため、入力のたびに再レンダリングが走る。リアルタイムバリデーションや入力依存の UI に適している - 非制御コンポーネントは
defaultValue+refを使い、DOM が値を所有する。送信時にのみ値を読み取れば十分なシンプルなフォームで再レンダリングを減らせる react-hook-formは内部で非制御コンポーネントを採用し、登録(register)した input をrefで管理することで大量フィールドでのパフォーマンスを確保している- ファイル input(
<input type="file">)は常に非制御。valueで制御できないためrefかe.target.filesで取得する - どちらを選ぶかの基準: 入力中の値に基づいて何かする必要があれば制御、なければ非制御で十分