React 学习站
React›核心›核心

受控与非受控组件

核心核心

一句话定义

受控组件:表单元素的值由 React state 驱动(value={v} onChange={...});非受控组件:值留在 DOM 自己手里,React 只在需要时用 ref 去取。绝大多数场景选受控。

为什么重要

表单是前端应用中最密集的「状态 ↔ 输入」交互,两种路线的选择影响校验、联动、可测试性。同时这对概念是理解 React 数据流「单一事实来源(Single Source of Truth)」的最佳教具,也适用于 checkbox、select、自定义输入组件。

前置知识

  • kp-005 state 快照。
  • kp-006 onChange 事件。
  • kp-017 ref 的基本用法(非受控部分用到)。

核心概念

  • 单一事实来源:受控组件里,输入框显示什么由 state 决定,输入只是「提议」,state 接受后才生效。
  • defaultValue:非受控的初始值,之后 DOM 自治。
  • file input:天生非受控(React 无法替你持有文件对象),用 ref 读取。
  • 即时校验 / 禁用提交 / 格式化输入:需要值参与渲染 → 受控。
  • 提交时才需要值:非受控更省事。

原理 / 机制

同一需求的两种实现:

jsx// 受控:value 在 state,UI 是状态的投影
function Controlled() {
  const [text, setText] = useState('');
  return (
    <input value={text} onChange={e => setText(e.target.value)} />
    // text 可用于:长度提示、实时校验、禁用按钮、防抖搜索
  );
}

// 非受控:值在 DOM,ref 取用
function Uncontrolled() {
  const inputRef = useRef(null);
  return (
    <>
      <input defaultValue="" ref={inputRef} />
      <button onClick={() => console.log(inputRef.current.value)}>读取</button>
    </>
  );
}

受控的关键能力演示——「值不总是采纳」:

jsx// 只允许数字:state 过滤后才写回,输入框永远不会出现字母
function NumInput() {
  const [v, setV] = useState('');
  function onChange(e) {
    if (/^\d*$/.test(e.target.value)) setV(e.target.value);
  }
  return <input value={v} onChange={onChange} />;
}

这在非受控下几乎无法实现——因为显示由 DOM 决定,React 无权否决用户敲的字符。

全表单的受控汇总与提交:

jsxconst [form, setForm] = useState({ name: '', email: '' });
const valid = form.name.length > 0 && form.email.includes('@');

<form onSubmit={e => { e.preventDefault(); if (valid) submit(form); }}>
  <input value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} />
  <input value={form.email} onChange={e => setForm({ ...form, email: e.target.value })} />
  <button disabled={!valid}>提交</button>
</form>

直观类比

受控像报账制:你每花一笔钱(敲一个字)都要先报销进公司账本(state),界面显示的是账本余额;审计(校验/回显/测试)随查随有。非受控像零用金:钱在兜里,花起来快(少一次 state 更新),但月底要对账(提交时用 ref 捞值),中间过程不可见。

实例 / 案例

选择两种路线的真实场景对照:

场景推荐理由
登录/注册表单受控实时校验、错误提示、按钮禁用
搜索框 + 防抖受控query 是后续请求的输入
一万行的配置导入界面受控 + 库需要整体校验/联动(或表单库)
「立即评价」的纯文本域,提交即走非受控值只在提交时需要
文件上传非受控file 对象不可由 React 持有
第三方组件嵌入(如日期选择器)视其 API好的组件库两者都提供

大型表单不要手搓受控状态——用 react-hook-form(内部以非受控为主、性能好)或 Formik(全受控)——见 kp-021 生态说明。

常见误区

  • 受控输入忘绑 onChange:<input value={text} /> 固定死值,敲字无效(React 还会警告)。
  • 受控与非受控来回切换:value={v ?? undefined} 这种条件导致 React 警告且行为混乱;一个元素选定一种模式。
  • 每次 onChange 里 setState 导致「输入卡顿」:其实是重渲染成本高,先查渲染链(kp-019),而不是怀疑受控模式。
  • 以为受控 = 性能差:受控多一次渲染是事实,但输入框重渲染成本极低;真正的开销通常在列表/图表层。

自测题

  1. 用「单一事实来源」解释为什么受控输入可以实现「过滤非法字符」。
  2. 什么情况下必须非受控?举两个例子并说明受控为什么做不到。
  3. 把上面的受控表单改成非受控版本,指出你失去了哪些能力。
  4. value={text} 且没有 onChange,输入时发生什么?控制台说什么?

与其他知识点的关系

  • 向前:是 kp-004「状态提升」在表单域的标准应用。
  • 向后:kp-017 的 useRef 是非受控的钥匙;kp-025 的 React 19 Actions 提供了「表单提交」的新范式(Server Action / useActionState),会改变一部分受控表单的写法;kp-021 提到表单库选型。

延伸阅读

  • Controlled and uncontrolled components(官方 FAQ 视角):<https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components>
  • react-hook-form(生态参考):<https://react-hook-form.com/>