1、React表单基础:受控组件与非受控组件的概念与区别

表单处理,是每个React开发者都绕不开的话题。我记得刚接触React时,就被「受控」和「非受控」这两个词搞得有点懵。说白了,它们就是两种管理表单数据的方式——一个交给React管,一个交给DOM自己管。

今天我们就来彻底搞清楚这两个概念,以及在实际项目中该怎么选。

1.1 受控组件:数据交给React

受控组件,就是表单的值由React state来驱动。你想想看,输入框里显示什么,不取决于用户敲了什么,而取决于state里存了什么。

function ControlledInput() {
  const [value, setValue] = useState('');

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

  return (
    <input
      type="text"
      value={value}
      onChange={handleChange}
    />
  );
}

这里有个关键点:value 属性绑定了 state,onChange 负责更新 state。用户每次敲键盘,流程都是:输入框触发 onChange → setValue 更新 state → 组件重新渲染 → 输入框显示新值。

核心特征:React state 是「唯一数据源」。表单的值完全由 React 控制。

我在项目中遇到过这样一个场景:需要实时校验用户输入的邮箱格式。用受控组件就特别方便——每次 onChange 时都能拿到最新值,立刻做校验,然后决定是否显示错误提示。

1.2 非受控组件:交给DOM自己管

非受控组件就简单多了。表单的值由 DOM 自己维护,React 不插手。你需要取值时,通过 ref 去 DOM 里拿。

function UncontrolledInput() {
  const inputRef = useRef(null);

  const handleSubmit = () => {
    console.log('当前输入值:', inputRef.current.value);
  };

  return (
    <div>
      <input type="text" defaultValue="默认值" ref={inputRef} />
      <button onClick={handleSubmit}>提交</button>
    </div>
  );
}

注意这里用的是 defaultValue 而不是 valuedefaultValue 只在初次渲染时生效,之后用户怎么改,React 都不管。

小技巧:非受控组件适合「一次性取值」的场景,比如表单提交时统一读取所有字段。

1.3 两者的核心区别

我习惯用一张表来对比,这样更直观:

对比维度 受控组件 非受控组件
数据源 React state DOM 自身
取值方式 直接从 state 读取 通过 ref 读取 DOM
初始值设置 state 初始值 defaultValue / defaultChecked
实时校验 天然支持 需要额外监听事件
代码量 稍多 较少
性能 每次输入都触发渲染 不触发渲染

为什么会这样?说白了,受控组件每次 state 变化都会触发重新渲染,而非受控组件只在你主动通过 ref 取值时才和 React 交互。

1.4 如何选择合适的场景

这个问题,我个人的经验是:优先用受控组件,除非有明确的理由用非受控

具体来说:

  • 需要实时校验的场景——比如密码强度检测、邮箱格式验证,用受控组件。我在项目中做过一个注册表单,密码输入时要实时显示强度条,非受控根本没法做。
  • 需要联动其他字段——比如选择国家后自动填充区号,受控组件让联动变得很简单。
  • 简单的、一次性的表单——比如搜索框、文件上传,用非受控就够了。你想想看,搜索框用户输完直接提交,中间不需要任何校验,何必多写一堆 state 呢?
  • 性能敏感的大表单——如果表单有上百个输入框,每个都受控会导致频繁渲染。这时候可以考虑用非受控,或者配合 useRef 做局部控制。

避坑指南:我曾经在一个项目中,把文件上传 input 写成了受控组件,结果发现 value 属性无法被程序设置(浏览器安全限制),导致上传功能直接失效。嗯,文件上传这种场景,老老实实用非受控吧。

1.5 混合使用也是可以的

别以为一个表单里只能选一种。我经常在一个表单里混用:大部分字段用受控,少数简单的字段用非受控。

function MixedForm() {
  const [email, setEmail] = useState('');
  const fileRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    const file = fileRef.current.files[0];
    console.log('邮箱:', email, '文件:', file.name);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input type="file" ref={fileRef} />
      <button type="submit">提交</button>
    </form>
  );
}

这样既享受了受控组件的实时校验能力,又避免了文件上传的坑。

1.6 小结

受控组件和非受控组件,没有绝对的好坏。我的建议是:默认选受控,简单场景选非受控,复杂场景可以混用。你只要记住一点——受控组件让 React 掌控数据流,非受控组件把控制权还给 DOM。理解了这一点,你就知道该怎么选了。

下一章,我们会深入表单校验的实战技巧,包括如何封装可复用的校验规则、如何处理异步校验等。到时候我会分享一些我在大厂项目中踩过的坑,敬请期待。