Menu

Reactの制御コンポーネントと非制御コンポーネントの違い

制御された入力は値をReactのstateから受け取り、onChangeを通じてだけ変わります。非制御の入力は自分の値をDOMに持ち、refやFormDataで読み取ります。違い、よくある警告、それぞれの使いどころを学びます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

制御コンポーネントは、値がReactのstateにあるフォーム要素です。value を渡し、onChange でstateを更新するので、入力欄に何を表示するかはReactが決めます。非制御コンポーネントは、素のHTMLのように値をDOMに持ちます。初期値の defaultValue を渡し、現在の値は後でrefや FormData を通じて読みます。

両方の入力欄に入力してください。段落は1つ目の入力欄にキーを押すたびに追従します。変更のたびにstateがセットされ、再レンダリングされるからです。2つ目の入力欄はDOMの中でだけ変わります。Reactはレンダリングせず、その値が見えるのは「Read both」が尋ねたときだけです。

制御された入力

制御された入力では、Reactのstateが唯一の情報源です。キーを押すたびの流れは次のとおりです。

  1. ユーザーが入力し、ブラウザがinputイベントを発生させます。
  2. onChange ハンドラが e.target.value を読み、セッターを呼びます。
  3. Reactがもう一度レンダリングし、新しいstateを入力欄の value に書き戻します。

このループの途中に自分のコードがあるので、非制御の入力にはできないことができます。文字を拒否したり形を変えたりする、値を別の場所に表示する、値が有効になるまでボタンを無効にする、コードから値を設定する(「Clear」ボタンや、ユーザーがクリックする候補)などです。

文字を入力してみてください。決して表示されません。ハンドラがstateをセットする前に数字以外を取り除くからです。Verifyボタンは、ちょうど6桁になるまで無効のままです。

フォームのページの例が使っているのは制御された入力で、ほとんどのReactのコードでも標準的な選択です。

非制御の入力

非制御の入力は素のHTMLのように動きます。Reactは defaultValue(チェックボックスやラジオなら defaultChecked)で1回描画し、その後は手を出しません。値を読むには、要素へのrefを持っておくか、送信時に FormData でフォームの名前付きの項目をすべて読みます。

項目のためのstateも onChange もありません。値はフォームが送信されたときにだけ集められます。チェックボックスのチェックを外してもう一度送信すると、subscribe のキーが消えます。チェックされていないチェックボックスは、そもそも FormData に含まれないからです。

最初のレンダリングの後に defaultValue を変えても、入力欄には何も起きません。Reactがそれを適用するのは要素が作られるときだけだからです。非制御のフォームをリセットするには、e.target.reset() を呼ぶか、フォームに新しい key を与えてReactに新しい入力欄を作らせます。

入力できない入力欄

value を渡して onChange を渡さないと、入力欄は制御されているのに、stateを更新するものが何もありません。Reactはキーを押すたびに同じ値を書き戻すので、入力欄は固まったように見えます。

それぞれの欄に入力してみてください。1つ目は変わりません。2つ目は制御された入力の正しい直し方で、そこに入力すると、1つ目と最後の欄もそれに従います。3つとも同じstateを読んでいるからです。3つ目は非制御なので入力を受け付けます。4つ目は、編集できないことを意図的に示しています。

開発環境では、Reactは1つ目の入力欄について「You provided a value prop to a form field without an onChange handler」という警告を記録します。プレビューは本番ビルドのように動くのでここではその警告は表示されませんが、自分の開発サーバーでは表示されます。

非制御から制御への切り替え

Reactは value を見て、入力欄が制御されているかどうかを判断します。value が undefined か null なら、その入力欄は非制御です。そのため、value が undefined で始まって後で文字列になる入力欄は、途中で非制御から制御へ切り替わり、開発環境でReactは「A component is changing an uncontrolled input to be controlled」と警告します。

この警告は本物のバグを指しています。逆に文字列から undefined に戻ると、入力欄はまた非制御になり、Reactはそこに書き込むのをやめます。

両方の入力欄に何か入力し、それぞれのClearボタンを押してください。2つ目の入力欄は空になります。1つ目はテキストが残ります。stateを undefined にしたことで非制御になり、Reactはもう表示を管理しないからです。

undefined の原因としてよくあるのは、まだ読み込まれていないデータ(ユーザーが届く前の value={user.name})や、まだ存在しないオブジェクトのフィールドです。直し方は2つあります。

const [name, setName] = useState('');   // start with a string
<input value={user?.name ?? ''} />       // or fall back to one

チェックボックスでは同じルールが checked に当てはまります。undefined ではなく false で始めてください。

ファイル入力は常に非制御

<input type="file"> は制御できません。ブラウザがページにその値の設定を許さないからです(ディスク上のファイルを選べるページはセキュリティの穴になります)。選ばれたファイルは、onChange で e.target.files から読むか、フォームの送信時にrefを通じて読みます。

コンピューターからファイルを1つか2つ選んでください。何もアップロードされず、例は名前とサイズを一覧にするだけです。ファイル入力に設定できる唯一の値は空文字列で、Clearボタンはそれでリセットしています。

使い分け

制御非制御
値の置き場所ReactのstateDOM
初期値の設定value / checkeddefaultValue / defaultChecked
値の読み方いつでもstateからrefで、または送信時に FormData で
キーを押すたびのレンダリングありなし
コードから値を変えるstateをセットするrefを通じてDOMに書き込む
向いている用途リアルタイムのバリデーション、整形、依存する項目送信時に読む簡単なフォーム、ファイル入力、フォームアクション

ユーザーの入力中に画面が値に反応する必要があるなら、制御を選びます。最後に値が必要なだけなら非制御を選びます。React 19のフォームアクションもその方式で、<form action={fn}> は関数に FormData を渡し、その後で非制御の項目をリセットします。

キーを押すたびにレンダリングするのは重そうに聞こえますが、普通のフォームでは問題になりません。問題になるのは、大きくて遅いツリーが入力欄と一緒に再レンダリングされるときだけです。その場合はstateを入力欄の近くに置くか、遅い部分を独立したコンポーネントに移してください。

同じ2つの言葉は、自分のコンポーネントにも使われます。値と onChange をpropsで親から受け取るコンポーネントは親に制御されており、自分のstateを持つコンポーネントは非制御です。コンポーネントを制御されたものにするためにstateを上へ移すことが、stateのリフトアップのテーマです。

よくある質問

Reactの制御コンポーネントとは何ですか?

現在の値がReactのstateから来るフォーム要素で、value(または checked)で設定し、onChange で更新します。Reactのstateが唯一の情報源なので、値を常に把握でき、コードから変えることもできます。

Reactの非制御コンポーネントとは何ですか?

素のHTMLのように、自分の値をDOMに持つフォーム要素です。defaultValue(または defaultChecked)で初期値を与え、必要になったときにrefや送信時の FormData で現在の値を読みます。

Reactの入力欄に入力できないのはなぜですか?

onChange なしで value を渡したからです。入力欄は制御されているので、キーを押すたびにReactがstateの値を戻し続けます。stateを更新する onChange を追加するか、defaultValue に切り替えるか、本当に変えるべきでないなら readOnly を付けてください。

"A component is changing an uncontrolled input to be controlled" はどう直しますか?

入力欄の value が undefined(または null)で始まり、後で文字列になったのが原因です。常に文字列になるようにしてください。stateを '' で始めるか、value={name ?? ''} と書きます。

制御された入力と非制御の入力のどちらを使うべきですか?

ユーザーの入力中にUIが値に反応するときは制御された入力を使います。バリデーション、整形、無効化するボタン、互いに依存する項目などです。送信時にだけ値が必要なときや、常に非制御であるファイル入力には、非制御の入力を使います。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める