事件处理与表单
React 通过合成事件(SyntheticEvent)封装了浏览器原生事件,提供跨浏览器一致性。本章涵盖事件处理、表单受控/非受控模式、常用表单库。
合成事件(SyntheticEvent)
function Button() {
function handleClick(e) {
e.preventDefault(); // ✅
e.stopPropagation(); // ✅
console.log(e.type); // "click"
console.log(e.nativeEvent); // 原生浏览器事件
}
return <button onClick={handleClick}>Click</button>;
}| 特性 | 说明 |
|---|---|
| 跨浏览器 | 封装了浏览器差异,API 与原生一致 |
| 事件委托 | React 将所有事件委托到根节点(React 17 是 document,React 18+ 是 root 节点) |
| 池化(旧版) | React 16 中事件对象会被回收复用;React 17+ 已移除此行为 |
| 命名 | 驼峰命名:onClick, onChange, onSubmit |
事件处理模式
传递参数
// 箭头函数包裹
<button onClick={() => handleDelete(item.id)}>删除</button>
// 柯里化
<button onClick={handleDelete(item.id)}>
{/* 此时 handleDelete 需返回一个函数 */}
</button>阻止默认行为
function Form() {
function handleSubmit(e) {
e.preventDefault(); // 阻止表单默认提交(页面刷新)
// 处理提交逻辑
}
return <form onSubmit={handleSubmit}>...</form>;
}事件处理中的 this(仅 class 组件)
// class 组件三种绑定方式
class Form extends React.Component {
constructor() {
this.handleA = this.handleA.bind(this); // 方式1:构造器绑定
}
handleB = () => {} // 方式2:class fields(最推荐)
render() {
return <button onClick={() => this.handleC()}> {/* 方式3:箭头函数 */}
}
}onChange vs onInput
React 中 onChange 的行为与原生 DOM 不同——它与原生 onInput 行为一致(每次输入都触发):
// React 的 onChange ≈ 原生 onInput(每击键触发)
<input onChange={e => console.log(e.target.value)} />表单:受控组件(Controlled)
受控组件:表单数据由 React state 完全控制。
function ControlledForm() {
const [form, setForm] = useState({ name: '', email: '' });
function handleChange(e) {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
}
function handleSubmit(e) {
e.preventDefault();
console.log(form);
}
return (
<form onSubmit={handleSubmit}>
<input name="name" value={form.name} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
<button type="submit">提交</button>
</form>
);
}| 优点 | 缺点 |
|---|---|
| 单一数据源(state 即真相) | 代码量多(每个字段都要 value + onChange) |
| 实时验证、动态禁用方便 | 大表单时可能性能问题 |
| 数据传给其他组件简单 |
💡 最佳实践:对大多数业务表单,受控组件是首选方案。
不同表单元素的处理
// textarea(React 中用 value,而非 innerHTML)
<textarea value={text} onChange={e => setText(e.target.value)} />
// select
<select value={fruit} onChange={e => setFruit(e.target.value)}>
<option value="apple">苹果</option>
<option value="banana">香蕉</option>
</select>
// checkbox
<input
type="checkbox"
checked={isAgreed}
onChange={e => setIsAgreed(e.target.checked)}
/>
// radio
<input
type="radio"
value="male"
checked={gender === 'male'}
onChange={e => setGender(e.target.value)}
/>
// 文件上传(只能是非受控)
<input type="file" onChange={e => setFile(e.target.files[0])} />表单:非受控组件(Uncontrolled)
非受控组件:DOM 自身管理表单状态,React 仅在需要时读取。
function UncontrolledForm() {
const nameRef = useRef(null);
const emailRef = useRef(null);
function handleSubmit(e) {
e.preventDefault();
const data = {
name: nameRef.current.value,
email: emailRef.current.value,
};
console.log(data);
}
return (
<form onSubmit={handleSubmit}>
<input ref={nameRef} defaultValue="张三" />
<input ref={emailRef} />
<button type="submit">提交</button>
</form>
);
}| 特点 | 受控 | 非受控 |
|---|---|---|
| 数据管理方 | React state | DOM 自身 |
| value 属性 | 必须设 value |
用 defaultValue |
| 实时验证 | ✅ 容易 | ❌ 麻烦 |
| 代码量 | 多 | 少 |
| 适用 | 业务表单 | 简单表单、文件上传、与第三方库集成 |
💡 最佳实践:文件上传(
<input type="file">)必须是非受控——浏览器安全策略禁止 JS 设置其 value。
表单验证
手动验证
function validate(values) {
const errors = {};
if (!values.name) errors.name = '姓名必填';
if (!values.email.includes('@')) errors.email = '邮箱格式不正确';
return errors;
}常用表单库
| 库 | 特点 | NPM 周下载 |
|---|---|---|
| React Hook Form | 非受控 + ref、性能最优、代码最少 | ⭐ 最多 |
| Formik | 受控组件、功能完整、生态好 | 多 |
| TanStack Form | 与 TanStack Query 配合、类型安全 | 新起之秀 |
React Hook Form 快速示例
import { useForm } from 'react-hook-form';
function MyForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
const onSubmit = data => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name', { required: '姓名必填' })} />
{errors.name && <span>{errors.name.message}</span>}
<input {...register('email', {
pattern: { value: /^\S+@\S+$/i, message: '格式不正确' }
})} />
{errors.email && <span>{errors.email.message}</span>}
<button type="submit">提交</button>
</form>
);
}事件委托与 preventDefault 嵌套
function Outer() {
return (
<div onClick={() => console.log('outer')}>
<button onClick={e => {
e.stopPropagation(); // 阻止冒泡到 outer
console.log('inner');
}}>
Click
</button>
</div>
);
}🔬 深入原理:React 的事件委托是挂到 root 节点的。一个原生事件的传播路径是:
目标元素 → ... → document → root(React 在此处理)。在 React 事件处理器中调用e.stopPropagation()阻止的是 React 合成事件在组件树中的冒泡——底层原生事件实际上已经到达了 root。
🚨 常见陷阱
- 忘记
preventDefault():表单提交时页面会刷新 - value 变 undefined:受控 input 最初为 null/undefined 时,React 会警告 “A component is changing an uncontrolled input to controlled”
- onChange 拿到的 value 始终是字符串:数字、布尔需要用
Number()或e.target.checked - 异步 setState 导致验证滞后:用函数式更新
setForm(prev => ({...prev, [name]: value})) - 事件处理器中闭包过时:在 useEffect 中给事件处理器访问的 state/props 可能是旧的
- React 17+ 事件委托位置变化:从
document变为 root 节点,混合 React 和原生事件时注意 stopPropagation 行为