Skip to content
事件处理与表单

事件处理与表单

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。


🚨 常见陷阱

  1. 忘记 preventDefault():表单提交时页面会刷新
  2. value 变 undefined:受控 input 最初为 null/undefined 时,React 会警告 “A component is changing an uncontrolled input to controlled”
  3. onChange 拿到的 value 始终是字符串:数字、布尔需要用 Number()e.target.checked
  4. 异步 setState 导致验证滞后:用函数式更新 setForm(prev => ({...prev, [name]: value}))
  5. 事件处理器中闭包过时:在 useEffect 中给事件处理器访问的 state/props 可能是旧的
  6. React 17+ 事件委托位置变化:从 document 变为 root 节点,混合 React 和原生事件时注意 stopPropagation 行为