React 受控与非受控组件:表单场景怎么选

React 受控与非受控组件:表单场景怎么选

写表单的时候你八成遇到过这个纠结:输入框到底要不要绑 valueonChange?绑了,每敲一个字符都要 setState,几十个字段的大表单卡到怀疑人生;不绑,又不知道怎么拿到用户填的值。这就是受控(controlled)和非受控(uncontrolled)组件的核心分歧。这篇不讲概念背诵,直接从真实场景讲清楚什么时候用哪个。

先分清:值到底存在哪

判断一个组件是受控还是非受控,只看一件事:输入框显示的值,由谁掌管

  • 受控:值存在 React 的 state 里,DOM 只是 state 的投影。
  • 非受控:值存在 DOM 自己身上,React 平时不管,要用的时候临时去 DOM 里捞。
jsx 复制代码
// 受控: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

很多人第一次写受控组件会这么写:

jsx 复制代码
// 错误:只给了 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:

jsx 复制代码
// 想要受控:补 onChange
<input value={name} onChange={(e) => setName(e.target.value)} />

// 只想设初始值、不想管后续:用 defaultValue(非受控)
<input defaultValue={name} />

还有一个隐蔽版本:value={user.name},而 user.nameundefined,React 会把这个输入框当成非受控;等数据加载回来变成字符串,又切成受控,于是报 A component is changing an uncontrolled input to be controlled。解决办法是给个兜底:value={user.name ?? ""},让它从头到尾都是受控。

什么时候该用受控

受控的价值在于:输入的每一刻,你都能拿到值并据此做事。这些场景几乎只能用受控:

  • 实时校验(边打边提示"密码太弱")
  • 联动(选了省份,自动刷新城市下拉)
  • 格式化(手机号自动加空格、金额自动加千分位)
  • 提交按钮根据表单是否合法动态禁用
jsx 复制代码
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 type="file">------它天生只能非受控,文件值是只读的,你没法用 state 控制
  • 接入非 React 的第三方 DOM 库
jsx 复制代码
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 name="nickname" defaultValue="" />
      <input name="bio" defaultValue="" />
      <input type="file" name="avatar" /> {/* 文件只能非受控 */}
      <button>保存</button>
    </form>
  );
}

FormData + name 属性是非受控表单的黄金搭档:字段再多,提交逻辑也只有几行,而且完全没有逐字符 setState 的开销。

性能真相:别被"受控很慢"吓住

网上常说受控组件慢,这话只对了一半。慢的根源不是受控本身,而是你把整张大表单的 state 提到了一个组件里,导致每敲一个字符整个表单重渲染

解法不是放弃受控,而是把状态下沉:

jsx 复制代码
// 每个字段自己管自己的 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 = 受控(value + onChange),值存 DOM = 非受控(defaultValue + ref),两者别混用。
  • 要实时(校验、联动、格式化、动态禁用)→ 受控。
  • 纯收集、文件上传、接第三方 → 非受控,配 FormData 提交最省心。
  • 受控卡顿多半是状态放太高,把 state 下沉到字段就能救,不是非得改非受控。
  • 一句话记忆:需要"边打边反应"就受控,只要"最后拿结果"就非受控。
相关推荐
Damai1 小时前
Radix UI导致右侧白条
前端
weedsfly1 小时前
前端开发中的策略模式——告别 if-else 的优雅之道
前端·javascript·面试
不坑老师1 小时前
怎么在Word中制作汉字书写笔顺——不坑盒子汉字笔顺步骤在小学低段识字教学中的应用
前端·数据库·人工智能·word·excel·office
许我半盏清茶1 小时前
基于 Ollama + Vectra + MCP 从零搭建本地 RAG 知识库
javascript·人工智能·mcp
anyup1 小时前
30 分钟用 Trae Work + 魔珐星云做出会讲故事的 AI 3D 具身交互智能数字人
前端·ai编程·trae
IT_陈寒1 小时前
Vue的响应式竟然在这常见操作下失效了
前端·人工智能·后端
你怎么知道我是队长1 小时前
JavaScript 事件介绍
开发语言·前端·javascript
宇晨T2 小时前
从零搭建企业级Web服务:Shell脚本 + LNMP + Docker 实战指南
前端·docker·容器
禅思院2 小时前
AI对话前端从入门到崩溃:一个长对话引发的五层优化战争【四】
前端·架构·前端框架