React でフォームを作るとき、多くの解説では入力値を useState で管理する「制御コンポーネント」が紹介されます。しかし React には、入力値を state で持たずに DOM 自身に管理させる「非制御コンポーネント(Uncontrolled Components)」という選択肢もあります。この記事では、非制御コンポーネントとは何か、useRef と defaultValue を使った基本の書き方、制御コンポーネントとの違い、そしてどちらを選ぶべきかを、TypeScript の関数コンポーネント(TSX)のコードとあわせて解説します。
目次
入力値を state で持たない「非制御コンポーネント」
非制御コンポーネントとは、フォームの入力値を React の state で管理せず、DOM 要素自身に値を持たせるやり方です。ふだん HTML の <input> は、ユーザーが文字を打てばその値をブラウザ(DOM)が保持してくれます。非制御コンポーネントはこの仕組みをそのまま活かし、React は入力のたびに介入しません。値が必要になったとき(多くはフォーム送信時)に、ref を通して DOM から直接読み取ります。
これに対して制御コンポーネントは、入力のたびに onChange で state を更新し、その state を value に流し込むことで「React が値の唯一の持ち主」になります。非制御コンポーネントは逆に、値の持ち主を DOM に委ねる、と考えると分かりやすいでしょう。
useRef で入力欄を参照し、送信時に値を読む
非制御コンポーネントの基本は、useRef で <input> への参照を作り、送信時に ref.current.value で値を取り出すことです。useRef<HTMLInputElement>(null) のようにジェネリクスで要素の型を指定し、初期値には null を渡します。
import { useRef } from 'react';
export function SignupForm() {
// input 要素への参照を作る(初期値は null)
const nameRef = useRef<HTMLInputElement>(null);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// 送信時に DOM から直接値を読み取る
console.log('名前:', nameRef.current?.value);
};
return (
<form onSubmit={handleSubmit}>
{/* value も onChange も指定しない = 非制御 */}
<input type="text" ref={nameRef} />
<button type="submit">送信</button>
</form>
);
}
ポイントは、<input> に value も onChange も付けていないところです。入力中は React が一切関与せず、ユーザーが打った文字はそのまま DOM に保持されます。送信ボタンが押されて handleSubmit が呼ばれたときに初めて、nameRef.current?.value で現在の入力値を読み取ります。current は null の可能性があるため、オプショナルチェーン(?.)で安全にアクセスしています。
入力のたびに state を更新しないため再レンダリングが発生せず、コードもシンプルになります。「入力中はリアルタイムに値を見なくてよい。送信時にまとめて取れればよい」という場面にうまく合います。
初期値は value ではなく defaultValue で与える
非制御コンポーネントに初期値を設定したいときは、value ではなく defaultValue を使います。チェックボックスやラジオボタンなら defaultChecked です。value を指定すると、React はその要素を「制御コンポーネント」として扱おうとし、意図しない挙動や警告の原因になります。
import { useRef } from 'react';
export function ProfileForm() {
const nameRef = useRef<HTMLInputElement>(null);
const agreeRef = useRef<HTMLInputElement>(null);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log('名前:', nameRef.current?.value);
console.log('同意:', agreeRef.current?.checked);
};
return (
<form onSubmit={handleSubmit}>
{/* テキストの初期値は defaultValue */}
<input type="text" ref={nameRef} defaultValue="ゲスト" />
{/* チェックの初期値は defaultChecked */}
<label>
<input type="checkbox" ref={agreeRef} defaultChecked />
規約に同意する
</label>
<button type="submit">送信</button>
</form>
);
}
defaultValue と defaultChecked は「最初のレンダリング時の初期値」を指定するだけで、その後ユーザーが入力を変えても React は上書きしません。だからこそ、DOM が値の持ち主であり続けられます。一方 value は「常にこの値であってほしい」という指定なので、state と onChange をセットにしないと入力が固定されてしまいます。この違いが、制御と非制御を分ける核心です。
制御コンポーネントとの違いを整理する
ここまでの内容を踏まえて、制御コンポーネントと非制御コンポーネントの違いを表で整理します。「値をどこが持つか」と「初期値の指定方法」が大きな分かれ目です。
| 項目 | 制御コンポーネント | 非制御コンポーネント |
|---|---|---|
| 値を持つ場所 | React の state | DOM 要素自身 |
| 値の読み取り | state を参照 | ref.current.value で読む |
| 初期値の指定 | value に state を渡す | defaultValue / defaultChecked |
| 必要な props | value と onChange | ref(省略も可) |
| 入力ごとの再レンダリング | 発生する | 発生しない |
| リアルタイム検証・整形 | しやすい | しにくい |
制御コンポーネントは、入力のたびに state が更新されるため、文字数のカウント表示、入力に応じたボタンの有効・無効切り替え、全角を半角に整形するといった「入力中のリアルタイムな処理」が得意です。非制御コンポーネントはそうした処理には向きませんが、そのぶん記述量が少なく、パフォーマンス面でも軽くなります。
どちらを使うべきか
React の公式ドキュメントでは、基本的に制御コンポーネントが推奨されています。入力値を state として一元管理できるため、バリデーションや動的な UI 更新を組み込みやすく、アプリの状態と入力が常に一致するからです。迷ったらまず制御コンポーネントで書く、という方針で問題ありません。
そのうえで、非制御コンポーネントが向くのは次のような場面です。ファイル入力(<input type="file">)は仕様上ユーザーしか値を設定できず、常に非制御になります。また、jQuery ベースのプラグインなど React 外のライブラリと DOM を直接やり取りする場合や、入力中の処理が不要で「送信時に値がまとめて取れればよい」だけのシンプルなフォームでは、非制御のほうが素直に書けます。要は「入力中に値を監視する必要があるか」で選ぶとよいでしょう。
value と defaultValue の混在で起きる警告
非制御コンポーネントで最もつまずきやすいのが、value と defaultValue、あるいは制御と非制御の書き方が混ざってしまうケースです。それぞれ症状と対処を見ていきます。
value を付けたのに onChange がない
非制御のつもりで value を指定してしまうと、React はその要素を制御コンポーネントとみなします。しかし onChange がないため値を更新する手段がなく、入力しても文字が反映されない「読み取り専用」の状態になり、コンソールに警告が出ます。初期値を与えたいだけなら value ではなく defaultValue を使うのが正解です。
制御と非制御が途中で切り替わる
value={someState} のように書き、その state の初期値が undefined だと、最初は非制御として扱われ、後から値が入った瞬間に制御コンポーネントへ切り替わってしまいます。このとき React は「A component is changing an uncontrolled input to be controlled」という警告を出します。制御で書くなら state の初期値を空文字 '' にして最初から制御に統一し、非制御で書くなら value を使わず defaultValue にする、というように、1つの要素で制御か非制御かを最初から決めておくことが大切です。
ファイル入力は常に非制御になる
<input type="file"> は、セキュリティ上の理由からプログラム側で値を設定できません。そのため value による制御はできず、常に非制御コンポーネントとして扱う必要があります。選択されたファイルは ref を通して fileRef.current?.files から読み取ります。ファイルアップロードを扱うときは、この点を前提にコードを組み立てましょう。
まとめ
非制御コンポーネントは、入力値を React の state で管理せず DOM 自身に持たせ、useRef で作った参照から送信時に ref.current.value で値を読み取る方法です。初期値は value ではなく defaultValue(チェックボックスは defaultChecked)で与えます。制御コンポーネントは入力のたびに再レンダリングされリアルタイム検証に強い一方、非制御は記述量が少なく軽量です。React 公式は基本的に制御を推奨しており、迷ったら制御で書けば問題ありません。ただしファイル入力のように常に非制御になるものや、外部ライブラリ連携・シンプルなフォームでは非制御が適しています。value と defaultValue の混在による警告に注意し、1つの要素で制御か非制御かを最初から決めておきましょう。