受控组件与表单

表单是网页最常见的交互:登录、注册、搜索、提交评论。React 处理表单的方式和传统 HTML 有个本质区别——"受控组件":输入框的值完全由 React 的 state 控制。这一章我们彻底搞懂它。

1. 什么是受控组件

传统 HTML 里,输入框自己维护自己的值,你用 input.value 去读。React 不一样:它让 state 成为"唯一数据源",input 的 value 绑定 state,输入时通过 onChange 更新 state,这样数据形成闭环。

import { useState } from "react";

// 受控组件:input 的 value 绑定 state,onChange 同步更新
function NameInput() {
  const [name, setName] = useState("");

  return (
    <div>
      <input
        value={name}                        // 值来自 state
        onChange={e => setName(e.target.value)}  // 输入时更新 state
      />
      <p>你输入的是:{name}</p>
    </div>
  );
}

// 数据流:state → input.value(显示)
//        input → onChange → setState(更新)
// 形成单向循环,数据完全在 React 掌控中

这种写法初看啰嗦(为什么要绕一圈),但好处巨大:React 的 state 永远是"真相",你可以随时验证、格式化、限制输入。比如手机号只能填数字、密码长度限制、实时搜索建议——这些都是受控组件的功劳。

2. 各种表单元素的写法

不同表单元素绑定的属性略有不同,这里统一列出:

function AllInputs() {
  const [text, setText] = useState("");
  const [textarea, setTextarea] = useState("");
  const [select, setSelect] = useState("apple");
  const [checked, setChecked] = useState(false);
  const [gender, setGender] = useState("male");

  return (
    <form>
      {/* 1. 文本框:用 value */}
      <input value={text} onChange={e => setText(e.target.value)} />

      {/* 2. 文本域:用 value(不是原生的放在标签之间!) */}
      <textarea value={textarea} onChange={e => setTextarea(e.target.value)} />

      {/* 3. 下拉框:用 value 在 select 上(不是 option) */}
      <select value={select} onChange={e => setSelect(e.target.value)}>
        <option value="apple">苹果</option>
        <option value="banana">香蕉</option>
      </select>

      {/* 4. 复选框:用 checked,读 e.target.checked */}
      <input
        type="checkbox"
        checked={checked}
        onChange={e => setChecked(e.target.checked)}
      />

      {/* 5. 单选按钮:用 checked 比较 value */}
      <input
        type="radio"
        value="male"
        checked={gender === "male"}
        onChange={() => setGender("male")}
      />
      <input
        type="radio"
        value="female"
        checked={gender === "female"}
        onChange={() => setGender("female")}
      />
    </form>
  );
}

两个反直觉的地方:(1) textarea 在 React 里用 value 属性,而不是像原生那样把内容放在开闭标签之间;(2) selectvalue 放在 <select> 上(整体),不是某个 <option> 上。这两条记住,你就不会卡壳。

3. 多字段表单:用一个 state 对象

注册表单往往有用户名、邮箱、密码多个字段。如果每个都写一个 state 加一个 handler,代码会爆炸。推荐用一个 state 对象 + 一个通用 handler:

import { useState } from "react";

// 多字段表单:用一个 state 对象统一管理
function SignupForm() {
  const [form, setForm] = useState({
    username: "",
    email: "",
    password: ""
  });

  // 通用 handler:用动态属性名 [name] 处理所有 input
  const handleChange = (e) => {
    const { name, value } = e.target;
    setForm(prev => ({ ...prev, [name]: value }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log("提交:", form);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="username" value={form.username} onChange={handleChange} />
      <input name="email" value={form.email} onChange={handleChange} />
      <input name="password" type="password"
        value={form.password} onChange={handleChange} />
      <button type="submit">注册</button>
    </form>
  );
}

关键技巧是给每个 input 加 name 属性,handler 里用 e.target.name 拿到字段名,用计算属性名 [name]: value 更新对应字段。一个 handler 搞定所有输入框,无论多少字段都不乱。

4. 表单提交

表单的 onSubmit 在点击提交按钮或按回车时触发。一个标准的提交流程:

function ContactForm() {
  const [msg, setMsg] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();              // 1. 必加!阻止页面刷新
    if (!msg.trim()) {
      alert("内容不能为空");
      return;                       // 2. 校验失败提前退出
    }
    // 3. 发请求、调 API
    fetch("/api/contact", {
      method: "POST",
      body: JSON.stringify({ message: msg })
    });
    setMsg("");                     // 4. 提交后清空
  };

  return (
    <form onSubmit={handleSubmit}>
      <textarea value={msg} onChange={e => setMsg(e.target.value)} />
      <button type="submit">发送</button>
    </form>
  );
}

四步套路:(1) e.preventDefault() 阻止默认刷新;(2) 校验;(3) 发请求;(4) 清空表单。注意提交按钮要写 type="submit"(或不写,默认就是 submit),不要写 onClick——回车键提交是 onSubmit 才能处理的。

5. 实时校验

受控组件的甜头之一:可以实时校验。因为每次输入 state 都更新,你可以基于 state 派生校验信息:

这种"数据驱动校验"的体验远好于传统的"提交后才报错"。

6. 非受控组件(了解即可)

有时候受控组件确实啰嗦(比如一个简单的搜索框),React 也提供了"非受控"写法——用 ref 直接读 DOM:

import { useRef } from "react";

// 非受控组件:用 ref 直接读 DOM,不绑 state
function UncontrolledInput() {
  const inputRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log("输入的值:", inputRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={inputRef} defaultValue="默认值" />
      <button type="submit">提交</button>
    </form>
  );
}
// 适合:简单一次性表单(如搜索框)、集成非 React 代码

非受控组件的 input 不绑 state,React 不管它的值,你需要时用 ref.current.value 去读。这种写法适合一次性提交的简单表单,或集成非 React 代码。但日常开发 95% 的场景,推荐用受控组件——它的可控性更强。

7. 大型表单:用表单库

当表单字段几十个、校验规则复杂时,手写受控组件会累。这时用社区表单库:

中小项目手写受控组件够用,大型项目建议上 React Hook Form + Zod。

小结

受控组件 = state 控制 input value,onChange 同步更新。多字段用 state 对象 + 通用 handler。提交用 onSubmit,记得 e.preventDefault()。掌握受控组件,你就能做出任何复杂表单。下一篇看多页应用的灵魂——React Router

← 上一篇 Context 跨层传递

下一篇 React Router 路由

✈️💬