DrillLab
第 64 / 105 道64 / 105 · #329

受控组件 vs 非受控组件

Controlled component vs uncontrolled component

先自己答,再往下看Answer it yourself first

一句话:受控 = 值由 React state 说了算value + onChange);非受控 = 值由 DOM 自己保管, 需要时用 ref 去读。

受控非受控
值放哪React stateDOM 节点
怎么读直接读 stateref.current.value
每次输入重渲染不会
实时校验 / 联动容易
代码量

默认用受控。因为一旦要做「输入为空就禁用提交按钮」 「实时显示字数」「两个字段联动」, 受控是唯一顺手的方式 ——Q1 那道真题的校验要求就是这样

非受控的两个真实场景:<input type="file">只能非受控,出于安全 JS 不能设它的值); 以及性能敏感的大表单 (每次按键都重渲染整个表单时)。

会追问(高频):value 传了但没传onChange 会怎样?」—— 输入框变成只读, 打字没反应,React 还会警告。
「怎么给受控组件一个初始值又不锁死?」——defaultValue,但那就是非受控了。
value={undefined} 呢?」—— React 会把它当成非受控,然后在你后来传了值时报「从非受控变成受控」的警告。 所以初始值该写 "" 而不是undefined

In one line: controlled = React state owns the value (value + onChange); uncontrolled = the DOM keeps the value and you read it with a ref when you need it.

ControlledUncontrolled
Where the value livesReact stateThe DOM node
How you read itStraight from stateref.current.value
Re-renders on every keystrokeYesNo
Live validation / linked fieldsEasyHard
Amount of codeMoreLess

Default to controlled. The moment you need “disable submit while the field is empty”, a live character count, or two fields that react to each other, controlled is the only comfortable option — the validation requirement in the real Q1 question works exactly that way.

Two real cases for uncontrolled: <input type="file"> (it can only be uncontrolled — for security reasons JS cannot set its value); and performance-sensitive large forms, where every keystroke would otherwise re-render the whole form.

Follow-ups — these come up a lot: “What happens if you pass value but no onChange?” — the input goes read-only, typing does nothing, and React warns you.
“How do you give a controlled component an initial value without locking it?” — defaultValue, but that makes it uncontrolled.
“And value={undefined}?” — React treats it as uncontrolled, then warns you about switching from uncontrolled to controlled once you do pass a value. So the initial value should be "", not undefined.

JSX两种写法示意Illustrative
1// 受控:闭环 value -> onChange -> setState -> value
2const [text, setText] = useState(""); // 注意初始值是 "",不是 undefined
3<input value={text} onChange={(e) => setText(e.target.value)} />
4<button disabled={text.trim() === ""}>提交</button> {/* 校验只有受控才顺手 */}
5
6// 非受控:值在 DOM 里
7const ref = useRef(null);
8<input ref={ref} defaultValue="初始" />
9<button onClick={() => console.log(ref.current.value)}></button>
1// Controlled: a closed loop of value -> onChange -> setState -> value
2const [text, setText] = useState(""); // note the initial value is "", not undefined
3<input value={text} onChange={(e) => setText(e.target.value)} />
4<button disabled={text.trim() === ""}>Submit</button> {/* validation is easy only when controlled */}
5
6// Uncontrolled: the value lives in the DOM
7const ref = useRef(null);
8<input ref={ref} defaultValue="initial" />
9<button onClick={() => console.log(ref.current.value)}>Read</button>