React 受控与非受控组件:表单场景怎么选
React 受控与非受控组件:表单场景怎么选写表单的时候你八成遇到过这个纠结:输入框到底要不要绑value和onChange?绑了,每敲一个字符都要 setState,几十个字段的大表单卡到怀疑人生;不绑,又不知道怎么拿到用户填的值。这就是受控(controlled)和非受控(uncontrolled)组件的核心分歧。这篇不讲概念背诵,直接从真实场景讲清楚什么时候用哪个。先分清:值到底存在哪判断一个组件是受控还是非受控,只看一件事:输入框显示的值,由谁掌管。受控:值存在 React 的 state 里,DOM 只是 state 的投影。非受控:值存在 DOM 自己身上,React 平时不管,要用的时候临时去 DOM 里捞。// 受控:state 是唯一真相,DOM 跟着 state 走 function ControlledInput() { const [name, setName] useState(); return ( input value{name} // 值从 state 来 onChange{(e) setName(e.target.value)} // 每次输入回写 state / ); } // 非受控:值住在 DOM 里,用 ref 去读 function UncontrolledInput() { const inputRef useRef(null); const handleSubmit () { // 提交时才去 DOM 拿值,平时 React 不参与 console.log(inputRef.current.value); }; return ( input ref{inputRef} defaultValue / button onClick{handleSubmit}提交/button / ); }注意受控用value,非受控用defaultValue。两者混用是新手最常见的报错来源,下面会专门讲。一个经典的坑:value 写死却没给 onChange很多人第一次写受控组件会这么写:// 错误:只给了 value,没给 onChange input value{name} /React 会在控制台警告:You provided a value prop to a form field without an onChange handler。更要命的是这个输入框根本打不进字——因为value被 state 钉死了,而你没有任何地方去更新 state,DOM 永远显示旧值。正确写法要么补上onChange,要么你压根不想控制它就用defaultValue:// 想要受控:补 onChange input value{name} onChange{(e) setName(e.target.value)} / // 只想设初始值、不想管后续:用 defaultValue(非受控) input defaultValue{name} /还有一个隐蔽版本:value{user.name},而user.name是undefined,React 会把这个输入框当成非受控;等数据加载回来变成字符串,又切成受控,于是报A component is changing an uncontrolled input to be controlled。解决办法是给个兜底:value{user.name ?? },让它从头到尾都是受控。什么时候该用受控受控的价值在于:输入的每一刻,你都能拿到值并据此做事。这些场景几乎只能用受控:实时校验(边打边提示密码太弱)联动(选了省份,自动刷新城市下拉)格式化(手机号自动加空格、金额自动加千分位)提交按钮根据表单是否合法动态禁用function SignupForm() { const [email, setEmail] useState(); const isValid /^[^][^]\.[^]$/.test(email); return ( form input value{email} onChange{(e) setEmail(e.target.value)} / {/* 边输入边校验,这只有受控做得到 */} {!isValid email span邮箱格式不对/span} button disabled{!isValid}注册/button /form ); }这里isValid每次输入都会重算,提示和按钮状态实时更新。换成非受控,你只能在提交那一刻才知道对不对,体验差一截。什么时候该用非受控非受控的价值在于:React 不参与输入过程,省掉每次敲键的重渲染。适合这些场景:大表单、纯收集,提交时一次性读值,不需要实时反应文件上传input typefile——它天生只能非受控,文件值是只读的,你没法用 state 控制接入非 React 的第三方 DOM 库function Profile() { const formRef useRef(null); const handleSubmit (e) { e.preventDefault(); // 用原生 FormData 一次性把整张表单捞出来,不用给每个字段建 state const data new FormData(formRef.current); console.log(Object.fromEntries(data)); }; return ( form ref{formRef} onSubmit{handleSubmit} input namenickname defaultValue / input namebio defaultValue / input typefile nameavatar / {/* 文件只能非受控 */} button保存/button /form ); }FormDataname属性是非受控表单的黄金搭档:字段再多,提交逻辑也只有几行,而且完全没有逐字符 setState 的开销。性能真相:别被受控很慢吓住网上常说受控组件慢,这话只对了一半。慢的根源不是受控本身,而是你把整张大表单的 state 提到了一个组件里,导致每敲一个字符整个表单重渲染。解法不是放弃受控,而是把状态下沉:// 每个字段自己管自己的 state,输入只重渲染当前这个 Field function Field({ label }) { const [value, setValue] useState(); return ( label {label} input value{value} onChange{(e) setValue(e.target.value)} / /label ); } function BigForm() { // 父组件不持有各字段的值,输入不会触发父级重渲染 return ( form {fields.map((f) Field key{f.id} label{f.label} /)} /form ); }字段状态各自独立后,敲字只重渲染那一个Field,受控的性能问题基本消失。真正字段爆炸(上百个)且要统一管理时,再上 React Hook Form 这类库——它内部其实用非受控 ref 收集,兼顾了性能和体验,这也侧面说明两种模式没有绝对优劣。小结判断标准:值存 state 受控(valueonChange),值存 DOM 非受控(defaultValueref),两者别混用。要实时(校验、联动、格式化、动态禁用)→ 受控。纯收集、文件上传、接第三方→ 非受控,配FormData提交最省心。受控卡顿多半是状态放太高,把 state 下沉到字段就能救,不是非得改非受控。一句话记忆:需要边打边反应就受控,只要最后拿结果就非受控。