受控组件与表单
表单是网页最常见的交互:登录、注册、搜索、提交评论。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) select 的 value 放在 <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 派生校验信息:
- 邮箱格式:用正则
/^[^@]+@[^@]+\.[^@]+$/测试form.email。 - 密码强度:检查长度、是否含数字字母。
- 禁用提交按钮:
<button disabled={!isValid}>,校验通过前点不了。
这种"数据驱动校验"的体验远好于传统的"提交后才报错"。
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:性能好(非受控为主),API 简洁,自带校验。
- Formik:老牌,功能全,但 API 略繁琐。
- 配合 Zod 或 Yup 做 schema 校验。
中小项目手写受控组件够用,大型项目建议上 React Hook Form + Zod。
小结
受控组件 = state 控制 input value,onChange 同步更新。多字段用 state 对象 + 通用 handler。提交用 onSubmit,记得 e.preventDefault()。掌握受控组件,你就能做出任何复杂表单。下一篇看多页应用的灵魂——React Router。
← 上一篇 Context 跨层传递
下一篇 React Router 路由 →