🧠 React 表单双雄争霸:受控组件 vs 非受控组件,谁才是你真·项目搭子?

94 阅读5分钟

🌱 作者自白:

作为一个正走在“从小白到能打项目实战”的 React 学习者,你一定会遇到表单的各种花式输入框 —— 登录、注册、搜索、留言、上传、校验、联动……

看起来很基础,但这里面却藏着两个 React 世界观的终极哲学之争:受控组件(Controlled)和非受控组件(Uncontrolled)

如果你连它们都搞不清,表单做着做着,就会写得越来越混乱;如果你掌握了它们,React 表单将成为你代码中的一把利剑!

🌱 一、什么是受控组件?

“我掌控每一次输入的节奏,我知道它什么时候改变,也知道该不该让它改。”

这是 React 控制型表单的口吻。

📘 定义

受控组件指的是其表单值由 React 的 state 或其他状态管理机制所控制。

输入变化时,用户交互触发 onChange,进而触发 setState,然后 value 的值重新传回 input 元素。

✅ 代码示例

import { useState } from 'react';

function ControlledInput() {
  const [username, setUsername] = useState('');

  return (
    <input
      value={username}
      onChange={(e) => setUsername(e.target.value)}
      placeholder="请输入用户名"
    />
  );
}

🔍 流程解析

  1. input 的 value 绑定的是 React 状态;
  2. 用户每输入一个字,都会调用 setUsername 更新;
  3. 组件重新渲染,展示新状态。

🚨 特别提醒

如果你不给 value,React 会警告你:“你写的是非受控组件,但你又用了受控行为(比如onChange)。”


🌾 二、什么是非受控组件?

“我信任浏览器,我只在需要时看看它的值,其他时候你别来烦我。”

这是非受控组件的态度。

📘 定义

非受控组件是由原生 DOM 自己维护表单值,React 不主动参与值更新过程。

读取值的方式是使用 ref 引用,直接操作 DOM 元素。

✅ 代码示例

import { useRef } from 'react';

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

  const handleSubmit = () => {
    alert('你输入的是:' + inputRef.current.value);
  };

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

⚔️ 三、受控组件 vs 非受控组件,对比一览表

维度受控组件非受控组件
数据来源React 的 stateDOM 本身
更新方式每次输入更新状态不自动更新
性能影响输入时 re-render,略重不依赖 render,性能优
验证能力易于校验、联动等需手动获取值再校验
状态管理状态可控、可追踪无法统一追踪多个字段
文件上传支持不友好(file 类型无法受控)适合
第三方库支持✅ Formik / RHF✅ react-hook-form 更适配非受控
场景适配多用于验证、联动、复杂表单多用于简单表单、文件、性能优化

🔬 四、到底应该怎么选?

React 官方建议:尽可能使用受控组件,它更贴合 React 的声明式思想、数据单向流动、更利于未来扩展和测试。

但在下面场景中,非受控组件是更佳选择:

  1. 文件上传(input[type="file"]);
  2. 不需要频繁响应的简单表单(例如只在点击按钮时读取值);
  3. 集成第三方组件(如 jQuery 插件、CKEditor、地图控件);
  4. 性能优化场景(避免 re-render 导致卡顿);
  5. 迁移老代码或混合栈

🧪 五、项目实战对比:控制 vs 放养

场景 1:注册表单(使用受控组件)

需求:

  • 用户名必须填写;
  • 密码长度不少于 6;
  • 禁止非法字符;
  • 输入时显示提示;
function RegisterForm() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const handleRegister = () => {
    if (!username || password.length < 6) {
      alert('输入有误');
      return;
    }
    console.log({ username, password });
  };

  return (
    <>
      <input value={username} onChange={(e) => setUsername(e.target.value)} placeholder="用户名" />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="密码" />
      <button onClick={handleRegister}>注册</button>
    </>
  );
}

场景 2:文件上传表单(使用非受控组件)

function FileUploadForm() {
  const fileRef = useRef();

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

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

🚧 六、受控组件中的隐藏陷阱

🕳️ 陷阱 1:空字符串和 undefined 的切换

<input value={undefined} /> // 报错,value 必须是 string

React 不允许你混用“受控”与“非受控”状态,初值必须是空字符串或定义好的默认值。


🕳️ 陷阱 2:多个 input 共用一个 state 容器

const [form, setForm] = useState({ name: '', age: '' });

<input
  name="name"
  value={form.name}
  onChange={(e) =>
    setForm(prev => ({ ...prev, [e.target.name]: e.target.value }))
  }
/>

这种结构写多了容易忘记拆解更新逻辑,建议用表单库管理。


🔧 七、常用表单工具对受控/非受控的支持

主要方式优势
Formik基于受控组件成熟、文档完善、集成 Yup
react-hook-form基于非受控组件 + Proxy 技术轻量、性能强、支持 TS、无需 re-render
Zod / Yup验证库可配合 Formik/RHF,提供类型验证能力

🎤 八、面试必问问题拓展

❶ 什么是受控组件与非受控组件,它们的区别?

答:受控组件由 React 的状态控制输入值,通过 value 和 onChange 保持同步;非受控组件依赖 DOM 自身管理,使用 ref 读取值。受控组件更易于验证和联动,非受控适合性能敏感或文件上传等场景。


❷ React 中为什么推荐使用受控组件?

答:受控组件使得数据同步、表单验证、状态可追踪、组件调试更方便,符合 React 的声明式哲学。大型项目中也便于配合状态管理工具,如 Redux 或 Context。


❸ 实际项目中你使用过哪些表单管理工具?

答:我使用过 Formik 和 react-hook-form:

  • Formik 更贴合受控思想,易读易写;
  • react-hook-form 更轻量,适合性能要求较高的大表单,TS 体验也不错。

✍️ 九、总结一句话

“React 表单处理没有绝对的对错,只有更适合当前场景的选择。要响应联动,就用受控;要性能简单,就用非受控。