React 表单处理完全指南:从受控组件到表单验证

小飞兽 React 217 次阅读 2026-06-21

React 表单处理完全指南:从受控组件到表单验证

一、Introduction

表单是 Web 应用中最核心的交互形式之一——登录、注册、搜索、填写订单,全都需要表单。React 的表单处理和传统 HTML 表单有本质区别:React 通过"受控组件"(Controlled Components)模式,让表单数据完全由 React 的 state 驱动,实现精确的实时验证、条件逻辑和动态表单字段。

本文涵盖受控组件与非受控组件的核心区别、文本输入、复选框、单选按钮、下拉选择等常见表单元素的标准处理方式,以及基于 useReducer 的复杂表单状态管理方案。

---

二、受控组件基础

#### 2.1 什么是受控组件?

在 React 中,"受控"意味着表单元素的值完全由 React state 控制。每次用户输入都触发 state 更新,UI 随之变化;没有任何操作能绕过 React 直接修改 DOM 中的值。

import React, { useState } from 'react';

// 受控组件:输入框的值由 state 控制
function SimpleInput() {
  const [name, setName] = useState('');

  // onChange 事件 → 更新 state → 重新渲染 → UI 更新
  // 这个循环就是 React 表单的核心工作方式
  const handleChange = (e) => {
    setName(e.target.value);
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    alert(`提交的名字是:${name}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        姓名:
        <input
          type="text"
          value={name}             // 值由 state 控制
          onChange={handleChange}  // 必须配合 onChange 更新 state
          placeholder="请输入姓名"
        />
      </label>
      <button type="submit" style={{ marginLeft: 8 }}>提交</button>
      <p>当前输入:{name}</p>
    </form>
  );
}

// 非受控组件:表单值由 DOM 自己管理
function UncontrolledInput() {
  let inputRef = React.useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    // 通过 ref 直接读取 DOM 中的值
    alert(`非受控输入:${inputRef.current.value}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        邮箱(非受控):
        <input
          type="email"
          ref={inputRef}  // 用 ref 获取 DOM 引用
          defaultValue="user@example.com"  // 只有初始值,没有 value
        />
      </label>
      <button type="submit" style={{ marginLeft: 8 }}>提交</button>
    </form>
  );
}

---

三、复杂表单:useReducer + 表单逻辑分离

#### 3.1 用 useReducer 管理多字段表单

当表单字段超过 5-10 个,或者字段之间有复杂的依赖关系时,用 useState 会导致大量独立的 setState 函数,代码变得难以维护。useReducer 把所有字段的状态和更新逻辑集中管理,优势明显:

import React, { useReducer, useCallback } from 'react';

// ==================== 表单状态定义 ====================

const initialFormState = {
  username: '',
  email: '',
  password: '',
  confirmPassword: '',
  agreeTerms: false,
  role: 'developer',
  skills: [],
  errors: {},
  isSubmitting: false,
  submitSuccess: false
};

// reducer:集中管理所有字段的状态更新
function formReducer(state, action) {
  switch (action.type) {
    case 'SET_FIELD':
      return {
        ...state,
        [action.field]: action.value,
        // 清除该字段的错误提示(用户重新输入时)
        errors: {
          ...state.errors,
          [action.field]: undefined
        }
      };

    case 'SET_ERRORS':
      return { ...state, errors: action.errors };

    case 'CLEAR_ERROR':
      return {
        ...state,
        errors: { ...state.errors, [action.field]: undefined }
      };

    case 'SET_SUBMITTING':
      return { ...state, isSubmitting: action.value };

    case 'SET_SUCCESS':
      return { ...state, submitSuccess: action.value };

    case 'RESET':
      return initialFormState;

    default:
      return state;
  }
}

// ==================== 验证逻辑 ====================

function validateForm(formData) {
  const errors = {};

  if (!formData.username.trim()) {
    errors.username = '用户名不能为空';
  } else if (formData.username.length < 3) {
    errors.username = '用户名至少3个字符';
  }

  if (!formData.email) {
    errors.email = '邮箱不能为空';
  } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
    errors.email = '请输入有效的邮箱地址';
  }

  if (!formData.password) {
    errors.password = '密码不能为空';
  } else if (formData.password.length < 8) {
    errors.password = '密码至少8个字符';
  }

  if (formData.password !== formData.confirmPassword) {
    errors.confirmPassword = '两次输入的密码不一致';
  }

  if (!formData.agreeTerms) {
    errors.agreeTerms = '请同意服务条款';
  }

  return errors;
}

// ==================== 表单组件 ====================

function RegistrationForm() {
  const [state, dispatch] = useReducer(formReducer, initialFormState);

  const handleChange = useCallback((field) => (e) => {
    const value = e.target.type === 'checkbox'
      ? e.target.checked
      : e.target.value;
    dispatch({ type: 'SET_FIELD', field, value });
  }, []);

  const handleSubmit = async (e) => {
    e.preventDefault();

    // 1. 验证
    const errors = validateForm({
      username: state.username,
      email: state.email,
      password: state.password,
      confirmPassword: state.confirmPassword,
      agreeTerms: state.agreeTerms
    });

    if (Object.keys(errors).length > 0) {
      dispatch({ type: 'SET_ERRORS', errors });
      return; // 有错误,不提交
    }

    // 2. 提交
    dispatch({ type: 'SET_SUBMITTING', value: true });

    // 模拟 API 请求
    await new Promise(resolve => setTimeout(resolve, 1500));

    console.log('提交数据:', {
      username: state.username,
      email: state.email,
      password: state.password,
      role: state.role
    });

    dispatch({ type: 'SET_SUBMITTING', value: false });
    dispatch({ type: 'SET_SUCCESS', value: true });
  };

  const inputStyle = (field) => ({
    display: 'block',
    width: '100%',
    padding: 8,
    marginBottom: 4,
    border: `1px solid ${state.errors[field] ? 'red' : '#ddd'}`,
    borderRadius: 4
  });

  if (state.submitSuccess) {
    return (
      <div style={{ padding: 20, background: '#d4edda', borderRadius: 8 }}>
        <h3>✅ 注册成功!</h3>
        <p>欢迎 {state.username},请查收邮箱 {state.email} 激活账号。</p>
        <button onClick={() => dispatch({ type: 'RESET' })}>重新注册</button>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: 400 }}>
      <h2>用户注册</h2>

      <div style={{ marginBottom: 12 }}>
        <label>用户名</label>
        <input
          style={inputStyle('username')}
          type="text"
          value={state.username}
          onChange={handleChange('username')}
          placeholder="至少3个字符"
        />
        {state.errors.username && (
          <span style={{ color: 'red', fontSize: 12 }}>{state.errors.username}</span>
        )}
      </div>

      <div style={{ marginBottom: 12 }}>
        <label>邮箱</label>
        <input
          style={inputStyle('email')}
          type="email"
          value={state.email}
          onChange={handleChange('email')}
          placeholder="example@domain.com"
        />
        {state.errors.email && (
          <span style={{ color: 'red', fontSize: 12 }}>{state.errors.email}</span>
        )}
      </div>

      <div style={{ marginBottom: 12 }}>
        <label>密码</label>
        <input
          style={inputStyle('password')}
          type="password"
          value={state.password}
          onChange={handleChange('password')}
          placeholder="至少8个字符"
        />
        {state.errors.password && (
          <span style={{ color: 'red', fontSize: 12 }}>{state.errors.password}</span>
        )}
      </div>

      <div style={{ marginBottom: 12 }}>
        <label>确认密码</label>
        <input
          style={inputStyle('confirmPassword')}
          type="password"
          value={state.confirmPassword}
          onChange={handleChange('confirmPassword')}
          placeholder="再次输入密码"
        />
        {state.errors.confirmPassword && (
          <span style={{ color: 'red', fontSize: 12 }}>{state.errors.confirmPassword}</span>
        )}
      </div>

      <div style={{ marginBottom: 12 }}>
        <label>身份</label>
        <select
          style={inputStyle('role')}
          value={state.role}
          onChange={handleChange('role')}
        >
          <option value="developer">开发者</option>
          <option value="designer">设计师</option>
          <option value="manager">产品经理</option>
        </select>
      </div>

      <div style={{ marginBottom: 12 }}>
        <label>
          <input
            type="checkbox"
            checked={state.agreeTerms}
            onChange={handleChange('agreeTerms')}
          />
          {' '}我同意服务条款和隐私政策
        </label>
        {state.errors.agreeTerms && (
          <div style={{ color: 'red', fontSize: 12 }}>{state.errors.agreeTerms}</div>
        )}
      </div>

      <button
        type="submit"
        disabled={state.isSubmitting}
        style={{
          width: '100%',
          padding: 10,
          background: state.isSubmitting ? '#ccc' : '#007bff',
          color: '#fff',
          border: 'none',
          borderRadius: 4,
          cursor: state.isSubmitting ? 'not-allowed' : 'pointer'
        }}
      >
        {state.isSubmitting ? '提交中...' : '注册'}
      </button>
    </form>
  );
}

export default RegistrationForm;

---

四、运行效果说明

SimpleInput:在输入框输入文字,UI 实时显示"当前输入:xxx";提交时弹出 alert 显示输入内容。受控组件的特点:React state 是唯一真实来源,DOM 只是渲染结果。

RegistrationForm:填写所有字段后点击注册,1.5秒后显示注册成功页面;任一字段不符合规则时,在该字段下方显示红色错误提示;用户修改错误字段时,错误提示自动消失;点击"重新注册"重置所有字段。

---

五、常见问题(FAQ)

#### Q1:受控组件和非受控组件该怎么选择?

优先使用受控组件,因为它能让你实时访问表单数据、做即时验证、实现复杂的条件逻辑。只有在以下情况才考虑非受控组件:

1. 表单简单,不需要复杂验证
2. 需要操作 DOM(如文件上传 <input type="file">
3. 需要集成不支持受控模式的第三方库
4. 表单数据最终用 FormData 一次性获取

#### Q2:表单验证应该在前端做还是后端做?

两者都要做,且不能互相替代。 前端验证提升用户体验(即时反馈),后端验证保证数据安全(用户可以绕过前端 JS)。永远不要相信客户端的验证结果,后端必须重新验证。

#### Q3:多个输入框都调用同一个 onChange 处理函数,性能会有问题吗?

不会。现代浏览器的事件处理和 React 的批处理机制使得这种方式完全高效。如果你确实遇到性能问题(极端场景),可以考虑用 useCallback 配合 useReducer,或者使用 name 属性在单个 handler 中根据 e.target.name 分发到不同字段。

---

六、延伸阅读

  • [React Hooks 完全指南:useState 与 useEffect 核心用法]()
  • [React 自定义 Hooks 实战:逻辑复用与模块化]()
  • [React 条件渲染与列表渲染完全指南]()
  • [React 状态管理演进:从 useState 到 Redux]()

---