人人都会AI编程

10.4 受控组件与非受控组件的选型与实现

更新时间:2026-07-10

在 React 中处理表单元素时,你需要面对一个关键的设计决策:使用受控组件还是非受控组件。这两种模式决定了表单数据的状态由谁来管理,也直接影响代码的复杂度、性能和可维护性。

概念对比

| 特性 | 受控组件 | 非受控组件 |
|------|----------|------------|
| 数据存储 | 状态完全由 React state 控制 | 数据存放在真实 DOM 中,React 不直接管理 |
| 数据同步 | 每次输入都会更新 state 并触发重渲染 | 仅在需要时(如提交)通过 ref 读取 DOM 值 |
| 实现方式 | 设置 value/checked + onChange 处理 | 使用 defaultValue/defaultChecked + ref |
| 性能特征 | 每次按键都会重渲染,但数据与 UI 高度一致 | 无额外渲染开销,数据取值需手动读取 |
| 适用场景 | 需要实时校验、格式化、动态控制的表单 | 简单表单、大型表单性能优先、非 React 环境集成 |

受控组件:React 掌管一切

在受控组件中,表单元素的 value(或 checked)属性被绑定到组件的某个 state,并通过 onChange 事件即时更新该 state。React 成为所有输入数据的唯一数据源(Single Source of Truth)。

function ControlledInput() {
  const [name, setName] = useState('');

  const handleChange = (e) => {
    setName(e.target.value);
  };

  return (
    <div>
      <input
        type="text"
        value={name}
        onChange={handleChange}
        placeholder="输入姓名"
      />
      <p>当前输入:{name}</p>
    </div>
  );
}

受控组件的优势在于数据与 UI 的绝对同步。你可以方便地在 handleChange 中添加实时校验、格式化、限制输入等逻辑:

const handleChange = (e) => {
  // 只允许字母
  const onlyLetters = e.target.value.replace(/[^a-zA-Z]/g, '');
  setName(onlyLetters);
};

对于复选框、单选按钮、下拉选择等,同样遵循受控模式,只是使用的属性不同:

// 下拉选择
<select value={city} onChange={(e) => setCity(e.target.value)}>
  <option value="beijing">北京</option>
  <option value="shanghai">上海</option>
</select>

// 复选框
<input type="checkbox" checked={agree} onChange={(e) => setAgree(e.target.checked)} />

受控组件是 React 官方推荐的主流写法,尤其适合表单与 UI 其他部分紧密联动的场景。

非受控组件:放手让 DOM 自理

非受控组件将表单数据存储于真实 DOM 节点中,React 仅在需要时通过 ref 读取值。它的行为更接近传统的 HTML 表单。

function UncontrolledForm() {
  const nameRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    alert('提交的姓名:' + nameRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" ref={nameRef} defaultValue="默认名字" />
      <button type="submit">提交</button>
    </form>
  );
}

注意:初始化时可以使用 defaultValue 设置默认值,但后续的更新不会触发 React 重渲染。读取值时直接访问 ref.current.value

文件上传是一个典型的非受控场景,因为文件对象无法通过 value 属性直接绑定:

function FileUpload() {
  const fileRef = useRef(null);

  const handleUpload = () => {
    const file = fileRef.current.files[0];
    console.log('选中的文件:', file);
  };

  return (
    <div>
      <input type="file" ref={fileRef} />
      <button onClick={handleUpload}>上传</button>
    </div>
  );
}

选型指南:何时用哪种?

优先选择受控组件的场景:

  • 需要实时表单校验(如输入格式检查、密码强度提示)。
  • 需要动态控制 UI(如根据输入内容禁用/启用提交按钮、联动下拉选项)。
  • 需要格式化输入值(如手机号自动加空格、金额千分位分隔)。
  • 需要重置表单到初始状态时,直接重置 state 即可。

优先选择非受控组件的场景:

  • 大型表单,包含几十个字段且不需要即时反馈,减少大量 onChange 造成的重渲染。
  • 集成第三方非 React 库(如一些旧式 jQuery 插件),需要直接操作 DOM。
  • 文件输入等无法用 value 表达的数据类型。
  • 表单提交时才关心数据,如简单的搜索框、登录表单(可接受触发一次提交获取值)。

实际项目中的混合使用:
许多开发者倾向于受控组件的一致性,但也可以结合 React Hook Form 等库实现非受控模式的高性能 + 受控式的便捷校验。React Hook Form 内部采用 ref 获取表单值,避免了每次输入都重渲染整个表单,同时提供了 watcherrors 等实时响应能力,是高性能表单的最佳实践之一。

import { useForm } from 'react-hook-form';

function LoginForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  const onSubmit = (data) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register('username', { required: true })}
        placeholder="用户名"
      />
      {errors.username && <span>用户名必填</span>}
      <button type="submit">登录</button>
    </form>
  );
}

常见误区

  • “受控组件必须绑定 onChange”:如果只设了 value 而没有 onChange,输入框将变成只读状态,用户无法输入。React 要求受控组件必须提供更改处理逻辑。
  • 混用 defaultValue 和 value:React 中如果设置了 value 属性,组件会进入受控状态,此时 defaultValue 不生效。只能二选一。
  • 频繁更新的性能问题:受控组件每次按键都触发重渲染,虽然不是大问题,但在极端大型表单中可能造成卡顿。这时可考虑非受控或使用 React.memo/useCallback 优化子组件。

总结

受控与非受控是 React 表单处理的两个基本范式。受控组件提供了更强的控制力和数据一致性,是大多数场景的默认选择;非受控组件则以更少的模板代码和性能优势在某些场景中占优。掌握两者的实现方式和适用边界,能让你在面对不同需求时做出最合理的选型。