2、受控组件实战:使用useState管理表单输入,实现双向数据绑定
好,咱们直接进入正题。
上一章我们聊了受控组件和非受控组件的区别。说白了,受控组件就是把表单的「状态」交给 React 来管。那怎么管?最朴素的方式就是——useState。
我个人习惯把受控组件叫做「React 说了算的输入框」。你想想看,用户在输入框里敲一个字,这个字先经过 React 的 state,再渲染回输入框。这就是所谓的「双向数据绑定」。
2.1 从零开始:一个受控的输入框
先看最基础的例子。一个用户名输入框,用 useState 来管理它的值。
import React, { useState } from 'react';
function LoginForm() {
const [username, setUsername] = useState('');
const handleChange = (e) => {
setUsername(e.target.value);
};
return (
<div>
<label>用户名:</label>
<input
type="text"
value={username}
onChange={handleChange}
/>
<p>你输入的是:{username}</p>
</div>
);
}
这段代码里,value={username} 是数据流下去的管道,onChange={handleChange} 是事件传上来的通道。两者缺一不可。
我在项目中遇到过不少新手,只写了 value 没写 onChange,然后跑来问我「为什么输入框点不动?」——嗯,因为 React 把值锁死了,没有 onChange 去更新 state,输入框永远显示初始值。
2.2 处理多个输入字段
实际项目中,一个表单不可能只有一个字段。那怎么管理多个输入?
我建议用一个对象来存所有字段的值。这样代码更干净,也方便扩展。
function RegisterForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
return (
<form>
<input name="username" value={formData.username} onChange={handleChange} />
<input name="email" value={formData.email} onChange={handleChange} />
<input name="password" type="password" value={formData.password} onChange={handleChange} />
</form>
);
}
注意这里的关键:name 属性必须和 state 里的 key 对应。这样所有输入框可以共用同一个 handleChange 函数。
setFormData(prev => ({...prev, [name]: value})) 这种函数式更新,可以避免因闭包导致的 stale state 问题。我吃过这个亏,后来就养成了习惯。
2.3 处理不同类型的输入
不是所有输入都是文本框。复选框、单选按钮、下拉菜单,它们的取值方式不一样。
| 输入类型 | 取值属性 | 示例 |
|---|---|---|
| text / email / password | e.target.value | 普通文本输入 |
| checkbox | e.target.checked | 布尔值 |
| radio | e.target.value | 选中的值 |
| select | e.target.value | 选中的 option 值 |
举个例子,处理复选框:
const [agree, setAgree] = useState(false);
const handleCheckbox = (e) => {
setAgree(e.target.checked);
};
// 渲染
<input type="checkbox" checked={agree} onChange={handleCheckbox} />
这里用的是 checked 而不是 value。我刚开始做 React 时也搞混过,checkbox 用 value 去绑定,结果永远取不到正确的值。
2.4 双向数据绑定的本质
很多人听到「双向数据绑定」就想到 Angular 或者 Vue。其实 React 里的双向绑定是模拟出来的。
它的流程是这样的:
- 用户输入 → 触发 onChange 事件
- onChange 更新 state
- state 变化 → 组件重新渲染
- 新的 value 被设置到输入框
你看,这其实是一个单向循环。数据从 state 流向 UI,再从 UI 流回 state。只是这个循环转得足够快,用户感觉是「双向」的。
formData.username = e.target.value 这种操作,React 检测不到变化,不会重新渲染。我曾经在一个紧急项目里犯过这个错,排查了半天才发现是直接修改了对象。
2.5 实战:一个完整的登录表单
把上面的知识点串起来,写一个完整的登录表单:
function LoginForm() {
const [form, setForm] = useState({
email: '',
password: '',
rememberMe: false
});
const [errors, setErrors] = useState({});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
const val = type === 'checkbox' ? checked : value;
setForm(prev => ({
...prev,
[name]: val
}));
};
const handleSubmit = (e) => {
e.preventDefault();
// 这里做验证,下一章会详细讲
console.log('提交的数据:', form);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>邮箱:</label>
<input name="email" type="email" value={form.email} onChange={handleChange} />
</div>
<div>
<label>密码:</label>
<input name="password" type="password" value={form.password} onChange={handleChange} />
</div>
<div>
<label>
<input name="rememberMe" type="checkbox" checked={form.rememberMe} onChange={handleChange} />
记住我
</label>
</div>
<button type="submit">登录</button>
</form>
);
}
这个例子把 text、password、checkbox 三种类型统一处理了。核心就是根据 type 判断取 value 还是 checked。
2.6 什么时候不用受控组件?
说了这么多受控组件的好处,我也得坦白讲:不是所有场景都适合用受控组件。
比如:
- 文件上传(
<input type="file" />)—— 它的值是只读的,没法受控 - 简单的、不需要实时验证的搜索框
- 第三方编辑器(如富文本编辑器)
这些场景用非受控组件或者 ref 会更合适。我个人的原则是:只要需要实时获取输入值、做验证、或者联动其他组件,就用受控组件。否则,怎么简单怎么来。
下一章我们会在这个基础上,加上验证逻辑。到时候你会发现,受控组件做验证简直不要太方便——因为值就在 state 里,随时可以检查。