跳至正文
来两杯美式
返回

React 核心(12):表单与受控/非受控组件

By 来两杯美式
发布于

受控组件让 React 当表单的”大脑”,非受控组件让 DOM 自己管自己——选谁取决于你需要多少控制权。

一、两种哲学:谁来管表单的值?

React 世界里,表单元素(<input><textarea><select>)有个独特的身份问题:它的值到底归谁管?

这引出两种模式:

生活类比:

二、核心对比:一张表看清差异

维度受控组件非受控组件
数据源React stateDOM 自身
读取值从 state 读ref.current.value
实时验证✅ 每次 onChange 都能校验❌ 不方便,需额外监听
条件禁用提交✅ state 驱动,轻松控制❌ 需手动检查 ref
初始值value={state}defaultValue="..."
代码量稍多(state + onChange)较少(ref 即可)
适用场景需要实时反馈、复杂校验文件上传、简单表单、集成第三方

选择的核心逻辑:你需要对表单值做多少事? 需要实时干预 → 受控;只需要最终结果 → 非受控。

三、受控组件详解

3.1 基础用法:input / textarea / select

受控组件的公式:value 绑定 state + onChange 更新 state,两者成对出现。

function ControlledForm() {
  const [name, setName] = useState("");
  const [bio, setBio] = useState("");
  const [color, setColor] = useState("blue");

  return (
    <form>
      {/* input */}
      <input
        value={name} // 值由 state 决定
        onChange={e => setName(e.target.value)} // 变化时更新 state
      />

      {/* textarea —— 注意:React 用 value 而不是 children */}
      <textarea value={bio} onChange={e => setBio(e.target.value)} />

      {/* select —— value 绑在 select 上,不是 option 上 */}
      <select value={color} onChange={e => setColor(e.target.value)}>
        <option value="red">红色</option>
        <option value="blue">蓝色</option>
        <option value="green">绿色</option>
      </select>
    </form>
  );
}

注意 textareaselect 的写法——React 统一用 value 属性,这和原生 HTML 不同(原生 textarea 用标签内容,select 用 selected 属性)。

3.2 多个输入:用对象 state 管理

表单字段多了,一个字段一个 useState 会疯掉。用一个对象统一管理:

function SignupForm() {
  const [form, setForm] = useState({
    username: "",
    email: "",
    password: "",
  });

  // 通用更新函数——利用 ES6 计算属性名
  const handleChange = e => {
    const { name, value } = e.target;
    setForm(prev => ({ ...prev, [name]: value }));
  };

  return (
    <form>
      <input name="username" value={form.username} onChange={handleChange} />
      <input name="email" value={form.email} onChange={handleChange} />
      <input
        name="password"
        value={form.password}
        onChange={handleChange}
        type="password"
      />
    </form>
  );
}

关键技巧:name 属性和 state 的 key 同名,这样 handleChange 就能一个函数管所有字段。

3.3 实时验证模式

受控组件最大的优势:每次按键都能响应

function UsernameInput() {
  const [username, setUsername] = useState("");
  const [error, setError] = useState("");

  const handleChange = e => {
    const val = e.target.value;
    setUsername(val);

    // 实时校验
    if (val.length < 3) {
      setError("用户名至少 3 个字符");
    } else if (!/^[a-zA-Z0-9_]+$/.test(val)) {
      setError("只能包含字母、数字和下划线");
    } else {
      setError("");
    }
  };

  return (
    <div>
      <input value={username} onChange={handleChange} />
      {error && <span className="error">{error}</span>}
    </div>
  );
}

受控组件的实时验证不是”额外功能”,而是数据流自然的结果——state 变了,UI 就跟着变。

3.4 “完全控制”:value + onChange 成对出现

受控组件的铁律:valueonChange 必须成对出现

// ❌ 只写 value 不写 onChange —— 输入框将无法修改!
<input value={name} />
// React 会警告:你提供了 value 但没有 onChange,这个输入框是只读的

// ✅ 成对出现
<input value={name} onChange={e => setName(e.target.value)} />

// ✅ 如果确实想要只读,用 readOnly
<input value={name} readOnly />

为什么只写 value 会锁死输入?因为 React 把 value 当作唯一真相——你告诉它值是 name,它就永远是 name,用户打字也改不了(因为没有 onChange 告诉 React 更新 name)。

四、非受控组件详解

4.1 defaultValue:非受控的初始值

非受控组件不绑 value,而是用 defaultValue 设初始值,之后 DOM 自己管:

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

  return (
    <input
      defaultValue="初始内容" // 只在首次渲染生效
      ref={inputRef} // 用 ref 拿到 DOM 节点
    />
  );
}

defaultValuevalue 的关键区别:

4.2 ref 读取值

非受控组件的核心操作:需要值的时候,从 ref 读

function SimpleForm() {
  const nameRef = useRef(null);
  const emailRef = useRef(null);

  const handleSubmit = () => {
    // 提交时才读值
    console.log(nameRef.current.value);
    console.log(emailRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input defaultValue="" ref={nameRef} />
      <input defaultValue="" ref={emailRef} />
      <button type="submit">提交</button>
    </form>
  );
}

非受控组件的值”活在 DOM 里”——React 渲染树上看不到它,只有通过 ref 才能摸到。

4.3 文件上传:必须非受控

<input type="file">唯一必须用非受控的表单元素——因为 JavaScript 出于安全原因不能通过代码设置文件路径

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

  const handleSubmit = e => {
    e.preventDefault();
    const file = fileRef.current.files[0]; // 从 ref 读取文件
    console.log("选中的文件:", file.name);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="file" ref={fileRef} />
      <button type="submit">上传</button>
    </form>
  );
}
// ❌ 文件输入没有 value 属性,不能受控
<input type="file" value={something} />

// ✅ 只能用 ref
<input type="file" ref={fileRef} />

4.4 集成非 React 库

当需要把非 React 库(如 jQuery 插件、地图 SDK)挂到 DOM 元素上时,非受控模式是自然选择——React 不去管那个 DOM 节点的值,让第三方库自己操作:

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

  useEffect(() => {
    // 假设 flatpickr 是一个 jQuery 日期选择器
    flatpickr(inputRef.current, {
      dateFormat: "Y-m-d",
      onChange: dates => {
        console.log("选择的日期:", dates[0]);
      },
    });
  }, []);

  return <input ref={inputRef} />; // React 不干预,flatpickr 自己管
}

非受控组件是 React 和”外部世界”的接口——当 DOM 不完全归 React 管时,用 ref 桥接。

五、何时选择哪个?

5.1 默认推荐受控

大多数场景,用受控组件。 原因:数据流更可预测,调试更容易。

受控组件让表单值始终在 React 的可视范围内——你可以在 React DevTools 里看到 state,可以在任何时刻校验、转换、条件渲染。数据流是单向的、可追踪的。

5.2 非受控的适用场景

以下情况非受控更合适:

场景为什么非受控更好
文件上传JS 不能设置文件路径,必须非受控
简单一次性表单只需提交时读值,不需要实时反馈,省代码
集成非 React 库第三方库直接操作 DOM,React 不应干预
性能敏感的大量输入受控组件每次按键都触发 setState → 重渲染;非受控跳过这个开销

关于性能的补充说明:大多数表单不会有性能问题。只有当表单有几百个输入字段,或者每次按键触发昂贵的重渲染时,非受控才有实际优势。不要过早优化。

// 性能敏感场景示例:大量输入字段
function MassiveForm() {
  // ❌ 受控:每次按键都重渲染整个组件
  // const [fields, setFields] = useState({ ...100个字段 })

  // ✅ 非受控:只在提交时读值,避免频繁重渲染
  const formRef = useRef(null);
  const handleSubmit = e => {
    e.preventDefault();
    const formData = new FormData(formRef.current);
    // 遍历所有字段值
  };

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      ...
    </form>
  );
}

六、表单提交处理

无论受控还是非受控,表单提交的处理方式是一样的:监听 onSubmit + e.preventDefault()

function LoginForm() {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");

  const handleSubmit = e => {
    e.preventDefault(); // 阻止浏览器默认行为(页面刷新)
    // 受控:从 state 读值
    console.log({ username, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={username} onChange={e => setUsername(e.target.value)} />
      <input
        type="password"
        value={password}
        onChange={e => setPassword(e.target.value)}
      />
      <button type="submit">登录</button>
    </form>
  );
}
// ❌ 常见错误:忘记 preventDefault
const handleSubmit = (e) => {
  // 没有 e.preventDefault(),页面会刷新,所有数据丢失
  console.log(form)
}

// ❌ 把提交逻辑放在 button 的 onClick 上
<button onClick={handleSubmit}>提交</button>
// 问题:用户按 Enter 提交时不会触发 onClick

// ✅ 放在 form 的 onSubmit 上
<form onSubmit={handleSubmit}>
  <button type="submit">提交</button>
</form>
// 这样无论是点击按钮还是按 Enter,都会触发

e.preventDefault() 是表单提交的”安全带”——不系的话,浏览器会按默认行为刷新页面,你的 React 逻辑白跑了。

七、表单库简介

手写表单逻辑到一定复杂度后,你会想要库来帮忙。两个主流选择:

7.1 React Hook Form

7.2 Formik

7.3 选择思路

不要纠结”哪个更好”,而是看你的场景:

  • 表单大、字段多、性能敏感 → React Hook Form
  • 表单逻辑简单、需要实时反馈 → Formik 或手写受控
  • 刚学 React → 先手写受控,理解原理后再上库

两个库都只是减少样板代码的工具,底层原理还是受控/非受控。理解了本章内容,学任何表单库都不在话下。

八、常见陷阱

8.1 受控和非受控混用警告

// ❌ 同一个输入一会儿受控一会儿非受控
function BadInput() {
  const [value, setValue] = useState(undefined); // undefined → 非受控
  // 第一次渲染:value 是 undefined,React 认为是非受控
  // 用户输入后 setValue → value 有值了,React 认为是受控
  // 控制台警告:组件从非受控变为受控
  return <input value={value} onChange={e => setValue(e.target.value)} />;
}

// ✅ 初始值用空字符串,始终受控
function GoodInput() {
  const [value, setValue] = useState(""); // 空字符串 → 受控
  return <input value={value} onChange={e => setValue(e.target.value)} />;
}

8.2 textarea 的 children 陷阱

// ❌ 原生 HTML 习惯:内容写在标签里
<textarea>初始内容</textarea>

// ✅ React 受控写法:用 value
<textarea value={text} onChange={e => setText(e.target.value)} />

// ✅ React 非受控写法:用 defaultValue
<textarea defaultValue="初始内容" ref={textRef} />

8.3 select 的 value 陷阱

// ❌ 原生 HTML 习惯:在 option 上加 selected
<select>
  <option value="a">A</option>
  <option value="b" selected>B</option>  {/* React 中无效 */}
</select>

// ✅ React 受控写法:value 绑在 select 上
<select value={selected} onChange={e => setSelected(e.target.value)}>
  <option value="a">A</option>
  <option value="b">B</option>
</select>

九、决策流程图

面对一个表单字段,按这个顺序决策:

  1. 是文件上传吗? → 必须非受控
  2. 需要实时验证/条件渲染吗? → 受控
  3. 需要集成非 React 库吗? → 非受控
  4. 字段很多且性能敏感吗? → 考虑非受控或表单库
  5. 其他情况 → 默认受控

一句话:能受控就受控,不得不非受控才非受控。


一句话总结:受控组件让 React 掌控一切(state 驱动),非受控组件让 DOM 自治(ref 读取)——默认受控,文件上传和第三方集成用非受控,复杂表单上库。

相关


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
所属专题
React
第 12 / 17 篇
查看系列全部文章
  1. 01.React 核心(1):React 组件与函数的本质区别
  2. 02.React 核心(2):声明式与 React 的实现机制
  3. 03.React 核心(3):Props 完全指南
  4. 04.React 核心(4):useState 完全指南
  5. 05.React 核心(5):useEffect 完全指南
  6. 06.React 核心(6):useRef 完全指南
  7. 07.React 核心(7):Context API 与状态共享
  8. 08.React 核心(8):useCallback 与 useMemo
  9. 09.React 核心(9):Hooks 规则与闭包陷阱
  10. 10.React 核心(10):自定义 Hook 设计
  11. 11.React 核心(11):Key 与列表渲染
  12. 12.React 核心(12):表单与受控/非受控组件
  13. 13.React 核心(13):React 渲染机制深入——Fiber 与调度
  14. 14.React 核心(14):React 18 并发特性
  15. 15.React 核心(15):Suspense 机制详解
  16. 16.React 核心(16):TanStack Query 入门指南
  17. 17.React 核心(17):TanStack Query 缓存机制详解

上一篇
React 核心(13):React 渲染机制深入——Fiber 与调度
下一篇
React 核心(11):Key 与列表渲染