エラーの内容
ブラウザのコンソールを開くと、こんな警告が表示されています:
Warning: A component is changing an uncontrolled input of type 'text' to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component.
フォーム自体は一応動いています。しかしこの警告は、レンダリングの途中でインプットの性質が切り替わっていることを示しています。この切り替えが実際のバグを引き起こします。古い値の残留、バリデーションの誤動作、入力中にカーソルが末尾に飛ぶといった問題です。無視してはいけません。
なぜ発生するのか
Reactはインプットを2種類に明確に区別しています:
- 非制御(Uncontrolled):
valueプロパティなし — DOMが状態を管理する。 - 制御(Controlled):
valueがReactの状態に紐付けられている — Reactが唯一の情報源となる。
この警告は、インプットが非制御(value={undefined})として始まり、状態更新後に制御(value="something")へ切り替わったときに発生します。Reactはライフサイクルの途中でのこうした性質の変化を処理できません。
原因のほとんどは単純です。状態の初期値が空文字列ではなく、undefined や null になっているケースです。
修正手順
ステップ1 — 問題のあるインプットを特定する
警告にはたいていコンポーネント名が表示されます。表示されない場合は、React DevToolsを開き、レンダリング間で value が undefined と実際の文字列の間を行き来しているインプットを探してください。まず状態の初期化部分を確認しましょう。問題の90%はそこにあります。
ステップ2 — 状態を空文字列で初期化する
この変更だけで大半のケースが解決します。問題のあるパターンはこちら:
// BROKEN — value starts as undefined
const [name, setName] = useState();
const [email, setEmail] = useState(null);
return (
<form>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
</form>
);
修正後はこのようになります:
// FIXED — value starts as empty string
const [name, setName] = useState('');
const [email, setEmail] = useState('');
return (
<form>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
</form>
);
ステップ3 — オブジェクト形式のフォーム状態を修正する
多くのフォームは全フィールドを1つの状態オブジェクトにまとめています。その場合、各フィールドに明示的な文字列のデフォルト値が必要です。フィールドが欠けていると、暗黙的に undefined になってしまいます:
// BROKEN — form.email is undefined → uncontrolled on first render
const [form, setForm] = useState({ name: '' });
// FIXED — every field declared upfront
const [form, setForm] = useState({
name: '',
email: '',
phone: '',
message: ''
});
const handleChange = (e) => {
setForm(prev => ({ ...prev, [e.target.name]: e.target.value }));
};
ステップ4 — APIデータへの対策をする
フォームにユーザーデータを事前入力するためにフェッチすると、タイミングの問題が発生します。リクエストが進行中、user は null です。そのため、最初のレンダリング時にすべてのインプット値が undefined になります:
// BROKEN — user?.name is undefined while fetching
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(data => setUser(data));
}, []);
return (
<input type="text" value={user?.name} onChange={...} />
);
// FIXED — nullish coalescing as a safety net
return (
<input type="text" value={user?.name ?? ''} onChange={...} />
);
さらに良い方法として、user を最初から完全な形で初期化しておけば、フォールバックが不要になります:
const [user, setUser] = useState({ name: '', email: '', role: '' });
useEffect(() => {
fetchUser().then(data => setUser(data));
}, []);
ステップ5 — value と defaultValue を混在させない
初期値を持つ非制御インプットには defaultValue を使用します。同じインプットに value と defaultValue の両方を指定するのはよくあるミスです:
// Mixing controlled and uncontrolled — pick one
<input value={name} defaultValue="John" onChange={...} />
// Controlled — state drives everything
<input type="text" value={name} onChange={e => setName(e.target.value)} />
// Uncontrolled — DOM manages value, read it via ref
<input type="text" defaultValue="John" ref={inputRef} />
修正を確認する
- Chrome DevTools を開き、Consoleタブを選択します。
- ページをリロードしてフォームを操作します。入力、送信、フィールドのクリアを試してください。
A component is changing an uncontrolled inputという警告が消えているはずです。- React DevTools でコンポーネントを確認します。インプットの
valueプロパティが常に文字列であることを確認してください。undefinedでもnullでもないことが重要です。
まだ警告が表示される場合は、範囲を広げて確認してください。コンポーネントツリー全体で <input>、<textarea>、<select> 要素を検索しましょう。これらのいずれでも同じ警告が発生する可能性があります。
クイックチェックリスト
- 引数なしの
useState()→useState('')に変更する - テキストフィールドに
useState(null)→useState('')に変更する - フィールドが欠けているオブジェクト状態 → すべてのフィールドを
''デフォルト値で宣言する - APIデータを直接
valueに使用している →?? ''フォールバックを追加する - 同じインプットに
valueとdefaultValueの両方がある → どちらか一方を削除する - チェックボックスやラジオボタン?
checkedにはundefinedではなくfalseをデフォルト値として使用する
もう一つのエッジケース:チェックボックス
checked が undefined で始まる場合、チェックボックスでも同じ警告が発生します。修正方法は同じで、デフォルト値が異なります:
// BROKEN
const [agreed, setAgreed] = useState(); // undefined
<input type="checkbox" checked={agreed} onChange={e => setAgreed(e.target.checked)} />
// FIXED
const [agreed, setAgreed] = useState(false);
<input type="checkbox" checked={agreed} onChange={e => setAgreed(e.target.checked)} />
基本原則はこうです。インプットに value や checked プロパティを指定した時点で、Reactがそれを管理します。つまり、最初のレンダリングからReactの状態が有効な値である必要があります。テキストフィールドなら空文字列、チェックボックスなら false です。undefined や null を使うとDOMに制御が戻ってしまい、それがこの問題の始まりになります。

