跳到主要内容

表单处理

表单是 Web 应用中最常见的用户交互方式之一。React 提供了两种处理表单的主要方式:受控组件和非受控组件。理解它们的区别和适用场景,对于构建高效、可维护的表单至关重要。

受控组件与非受控组件

核心概念对比

特性受控组件非受控组件
数据来源React stateDOM 自身
数据更新通过 onChange + setState通过 ref 直接访问 DOM
值的验证实时验证提交时验证
适用场景复杂表单、实时验证简单表单、文件上传
代码复杂度较高较低

选择指南

  • 使用受控组件:需要实时验证、条件禁用、动态表单字段、表单值需要与其他 UI 同步
  • 使用非受控组件:文件上传、快速原型开发、集成非 React 代码、对性能有极致要求

受控组件详解

在受控组件中,表单数据完全由 React 组件的 state 控制。每个状态变更都有一个对应的事件处理器,这使得 React 能够成为"单一数据源"。

基本输入框

实时编辑器
function ControlledInput() {
  const [value, setValue] = useState('');

  const handleChange = (e) => {
    setValue(e.target.value);
  };

  return (
    <div>
      <input
        type="text"
        value={value}
        onChange={handleChange}
        placeholder="输入内容..."
        style={{ padding: '10px', width: '100%', boxSizing: 'border-box' }}
      />
      <p style={{ fontSize: '14px', color: '#64748b' }}>
        输入值: <strong style={{ color: '#3b82f6' }}>{value || '(空)'}</strong>
      </p>
    </div>
  );
}
结果
Loading...

多种表单控件

实时编辑器
function FormControlsDemo() {
  const [formData, setFormData] = useState({
    text: '',
    textarea: '',
    select: 'apple',
    checkbox: false,
    radio: 'option1',
    number: 0,
    date: '',
    color: '#3b82f6'
  });

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

  return (
    <div style={{ display: 'grid', gap: '15px' }}>
      {/* 文本输入 */}
      <div>
        <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>文本输入:</label>
        <input
          type="text"
          value={formData.text}
          onChange={handleChange('text')}
          style={{ padding: '8px', width: '100%', boxSizing: 'border-box' }}
        />
      </div>

      {/* 多行文本 */}
      <div>
        <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>多行文本:</label>
        <textarea
          value={formData.textarea}
          onChange={handleChange('textarea')}
          rows={3}
          style={{ padding: '8px', width: '100%', boxSizing: 'border-box' }}
        />
      </div>

      {/* 下拉选择 */}
      <div>
        <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>下拉选择:</label>
        <select 
          value={formData.select} 
          onChange={handleChange('select')}
          style={{ padding: '8px', width: '100%' }}
        >
          <option value="apple">苹果</option>
          <option value="banana">香蕉</option>
          <option value="orange">橙子</option>
        </select>
      </div>

      {/* 复选框 */}
      <div>
        <label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
          <input
            type="checkbox"
            checked={formData.checkbox}
            onChange={handleChange('checkbox')}
          />
          同意用户协议
        </label>
      </div>

      {/* 单选按钮组 */}
      <div>
        <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>单选按钮:</label>
        <div style={{ display: 'flex', gap: '15px' }}>
          {['option1', 'option2', 'option3'].map(opt => (
            <label key={opt} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
              <input
                type="radio"
                name="radioGroup"
                value={opt}
                checked={formData.radio === opt}
                onChange={handleChange('radio')}
              />
              选项 {opt.slice(-1)}
            </label>
          ))}
        </div>
      </div>

      {/* 数字输入 */}
      <div>
        <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>数字输入:</label>
        <input
          type="number"
          value={formData.number}
          onChange={handleChange('number')}
          style={{ padding: '8px', width: '100px' }}
        />
      </div>

      {/* 日期选择 */}
      <div>
        <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>日期选择:</label>
        <input
          type="date"
          value={formData.date}
          onChange={handleChange('date')}
          style={{ padding: '8px' }}
        />
      </div>

      {/* 预览 */}
      <div style={{ marginTop: '10px', padding: '10px', background: '#f1f5f9', borderRadius: '4px' }}>
        <strong>当前表单数据:</strong>
        <pre style={{ margin: '5px 0', fontSize: '12px' }}>{JSON.stringify(formData, null, 2)}</pre>
      </div>
    </div>
  );
}
结果
Loading...

处理多个输入

当表单有多个字段时,可以为每个字段添加 name 属性,使用单一的事件处理器:

实时编辑器
function MultiFieldForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: ''
  });

  // 统一的 change 处理器
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    alert(JSON.stringify(formData, null, 2));
  };

  return (
    <form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
      <div>
        <label style={{ display: 'block', marginBottom: '4px' }}>用户名:</label>
        <input
          name="username"
          value={formData.username}
          onChange={handleChange}
          style={{ padding: '8px', width: '100%', boxSizing: 'border-box' }}
        />
      </div>
      <div>
        <label style={{ display: 'block', marginBottom: '4px' }}>邮箱:</label>
        <input
          name="email"
          type="email"
          value={formData.email}
          onChange={handleChange}
          style={{ padding: '8px', width: '100%', boxSizing: 'border-box' }}
        />
      </div>
      <div>
        <label style={{ display: 'block', marginBottom: '4px' }}>密码:</label>
        <input
          name="password"
          type="password"
          value={formData.password}
          onChange={handleChange}
          style={{ padding: '8px', width: '100%', boxSizing: 'border-box' }}
        />
      </div>
      <button type="submit" style={{ padding: '10px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px' }}>
        提交
      </button>
    </form>
  );
}
结果
Loading...

非受控组件详解

非受控组件将数据存储在 DOM 中,而不是 React state 中。你可以使用 ref 来获取表单值。

基本用法

实时编辑器
function UncontrolledForm() {
  const inputRef = useRef(null);
  const textareaRef = useRef(null);
  const selectRef = useRef(null);
  const fileRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    const values = {
      input: inputRef.current?.value,
      textarea: textareaRef.current?.value,
      select: selectRef.current?.value,
      file: fileRef.current?.files?.[0]?.name
    };
    alert(JSON.stringify(values, null, 2));
  };

  return (
    <form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
      <div>
        <label style={{ display: 'block', marginBottom: '4px' }}>文本输入:</label>
        <input ref={inputRef} defaultValue="默认值" style={{ padding: '8px', width: '100%', boxSizing: 'border-box' }} />
      </div>
      <div>
        <label style={{ display: 'block', marginBottom: '4px' }}>多行文本:</label>
        <textarea ref={textareaRef} defaultValue="默认内容" rows={2} style={{ padding: '8px', width: '100%', boxSizing: 'border-box' }} />
      </div>
      <div>
        <label style={{ display: 'block', marginBottom: '4px' }}>下拉选择:</label>
        <select ref={selectRef} defaultValue="banana" style={{ padding: '8px' }}>
          <option value="apple">苹果</option>
          <option value="banana">香蕉</option>
          <option value="orange">橙子</option>
        </select>
      </div>
      <div>
        <label style={{ display: 'block', marginBottom: '4px' }}>文件上传:</label>
        <input ref={fileRef} type="file" />
      </div>
      <button type="submit" style={{ padding: '10px', background: '#22c55e', color: 'white', border: 'none', borderRadius: '4px' }}>
        提交
      </button>
    </form>
  );
}
结果
Loading...

defaultValue 和 defaultChecked

对于非受控组件,可以使用 defaultValuedefaultChecked 设置初始值:

// 文本输入
<input defaultValue="初始值" />

// 复选框
<input type="checkbox" defaultChecked={true} />

// 单选按钮
<input type="radio" defaultChecked={true} />

// 下拉选择
<select defaultValue="option2">
<option value="option1">选项 1</option>
<option value="option2">选项 2</option>
</select>

文件输入

文件输入是典型的非受控组件场景,因为文件输入的值是只读的,只能通过用户操作设置。

实时编辑器
function FileUploadDemo() {
  const fileRef = useRef(null);
  const [preview, setPreview] = useState(null);

  const handleFileChange = () => {
    const file = fileRef.current?.files?.[0];
    if (file && file.type.startsWith('image/')) {
      const reader = new FileReader();
      reader.onload = (e) => setPreview(e.target.result);
      reader.readAsDataURL(file);
    } else {
      setPreview(null);
    }
  };

  return (
    <div>
      <input
        ref={fileRef}
        type="file"
        accept="image/*"
        onChange={handleFileChange}
        style={{ marginBottom: '10px' }}
      />
      {preview && (
        <div>
          <img 
            src={preview} 
            alt="预览" 
            style={{ maxWidth: '200px', maxHeight: '200px', borderRadius: '8px' }} 
          />
        </div>
      )}
    </div>
  );
}
结果
Loading...

表单验证

表单验证是表单处理中的重要环节。我们可以在客户端进行基本的验证,提高用户体验。

实时验证

实时编辑器
function RealTimeValidation() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    confirmPassword: ''
  });
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  // 验证规则
  const validate = (name, value) => {
    switch (name) {
      case 'username':
        if (!value) return '用户名不能为空';
        if (value.length < 3) return '用户名至少 3 个字符';
        if (!/^[a-zA-Z0-9_]+$/.test(value)) return '只能包含字母、数字和下划线';
        return '';
      case 'email':
        if (!value) return '邮箱不能为空';
        if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return '邮箱格式不正确';
        return '';
      case 'password':
        if (!value) return '密码不能为空';
        if (value.length < 8) return '密码至少 8 个字符';
        if (!/[A-Z]/.test(value)) return '需要包含大写字母';
        if (!/[0-9]/.test(value)) return '需要包含数字';
        return '';
      case 'confirmPassword':
        if (!value) return '请确认密码';
        if (value !== formData.password) return '两次密码不一致';
        return '';
      default:
        return '';
    }
  };

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
    
    // 实时验证
    const error = validate(name, value);
    setErrors(prev => ({ ...prev, [name]: error }));
  };

  const handleBlur = (e) => {
    const { name } = e.target;
    setTouched(prev => ({ ...prev, [name]: true }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    
    // 验证所有字段
    const newErrors = {};
    Object.keys(formData).forEach(key => {
      newErrors[key] = validate(key, formData[key]);
    });
    setErrors(newErrors);
    setTouched({
      username: true,
      email: true,
      password: true,
      confirmPassword: true
    });

    // 检查是否有错误
    if (Object.values(newErrors).every(err => !err)) {
      alert('注册成功!\n' + JSON.stringify(formData, null, 2));
    }
  };

  const inputStyle = (name) => ({
    padding: '10px',
    width: '100%',
    boxSizing: 'border-box',
    border: touched[name] && errors[name] ? '2px solid #ef4444' : '1px solid #e2e8f0',
    borderRadius: '4px'
  });

  return (
    <form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
      <div>
        <label style={{ display: 'block', marginBottom: '4px', fontWeight: 'bold' }}>用户名:</label>
        <input
          name="username"
          value={formData.username}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder="3个字符以上,字母数字下划线"
          style={inputStyle('username')}
        />
        {touched.username && errors.username && (
          <p style={{ color: '#ef4444', fontSize: '12px', margin: '4px 0' }}>{errors.username}</p>
        )}
      </div>
      
      <div>
        <label style={{ display: 'block', marginBottom: '4px', fontWeight: 'bold' }}>邮箱:</label>
        <input
          name="email"
          type="email"
          value={formData.email}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder="[email protected]"
          style={inputStyle('email')}
        />
        {touched.email && errors.email && (
          <p style={{ color: '#ef4444', fontSize: '12px', margin: '4px 0' }}>{errors.email}</p>
        )}
      </div>
      
      <div>
        <label style={{ display: 'block', marginBottom: '4px', fontWeight: 'bold' }}>密码:</label>
        <input
          name="password"
          type="password"
          value={formData.password}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder="至少8位,包含大写字母和数字"
          style={inputStyle('password')}
        />
        {touched.password && errors.password && (
          <p style={{ color: '#ef4444', fontSize: '12px', margin: '4px 0' }}>{errors.password}</p>
        )}
      </div>
      
      <div>
        <label style={{ display: 'block', marginBottom: '4px', fontWeight: 'bold' }}>确认密码:</label>
        <input
          name="confirmPassword"
          type="password"
          value={formData.confirmPassword}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder="再次输入密码"
          style={inputStyle('confirmPassword')}
        />
        {touched.confirmPassword && errors.confirmPassword && (
          <p style={{ color: '#ef4444', fontSize: '12px', margin: '4px 0' }}>{errors.confirmPassword}</p>
        )}
      </div>
      
      <button 
        type="submit"
        style={{ 
          padding: '12px', 
          background: '#3b82f6', 
          color: 'white', 
          border: 'none', 
          borderRadius: '4px',
          cursor: 'pointer'
        }}
      >
        注册
      </button>
    </form>
  );
}
结果
Loading...

密码强度指示器

实时编辑器
function PasswordStrength() {
  const [password, setPassword] = useState('');

  const calculateStrength = (pwd) => {
    let score = 0;
    if (pwd.length >= 8) score++;
    if (pwd.length >= 12) score++;
    if (/[a-z]/.test(pwd)) score++;
    if (/[A-Z]/.test(pwd)) score++;
    if (/[0-9]/.test(pwd)) score++;
    if (/[^a-zA-Z0-9]/.test(pwd)) score++;
    return Math.min(score, 5);
  };

  const strength = calculateStrength(password);
  const labels = ['非常弱', '弱', '一般', '强', '非常强'];
  const colors = ['#ef4444', '#f97316', '#eab308', '#22c55e', '#10b981'];

  return (
    <div>
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="输入密码查看强度"
        style={{ padding: '10px', width: '100%', boxSizing: 'border-box' }}
      />
      {password && (
        <div style={{ marginTop: '10px' }}>
          <div style={{ display: 'flex', gap: '4px' }}>
            {[0, 1, 2, 3, 4].map(i => (
              <div
                key={i}
                style={{
                  flex: 1,
                  height: '6px',
                  borderRadius: '3px',
                  background: i < strength ? colors[strength - 1] : '#e2e8f0'
                }}
              />
            ))}
          </div>
          <p style={{ margin: '5px 0', fontSize: '12px', color: colors[strength - 1] || '#64748b' }}>
            密码强度: {labels[strength - 1] || '非常弱'}
          </p>
        </div>
      )}
    </div>
  );
}
结果
Loading...

React 19 表单 Actions

React 19 引入了 Actions 概念,极大简化了表单处理。你可以在 <form>action 属性中直接传递函数。

useActionState

useActionState 用于管理表单提交的状态。

实时编辑器
function ActionFormDemo() {
  const [state, formAction, isPending] = useActionState(
    async (prevState, formData) => {
      // 模拟 API 调用
      await new Promise(resolve => setTimeout(resolve, 1000));
      
      const name = formData.get('name');
      const email = formData.get('email');
      
      if (!name) {
        return { error: '请输入姓名' };
      }
      if (!email) {
        return { error: '请输入邮箱' };
      }
      
      return { success: true, message: `欢迎, ${name}!` };
    },
    null
  );

  return (
    <form action={formAction} style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
      <div>
        <label style={{ display: 'block', marginBottom: '4px' }}>姓名:</label>
        <input name="name" style={{ padding: '8px', width: '100%', boxSizing: 'border-box' }} />
      </div>
      <div>
        <label style={{ display: 'block', marginBottom: '4px' }}>邮箱:</label>
        <input name="email" type="email" style={{ padding: '8px', width: '100%', boxSizing: 'border-box' }} />
      </div>
      <button 
        type="submit" 
        disabled={isPending}
        style={{ 
          padding: '10px', 
          background: isPending ? '#94a3b8' : '#3b82f6', 
          color: 'white', 
          border: 'none', 
          borderRadius: '4px',
          cursor: isPending ? 'not-allowed' : 'pointer'
        }}
      >
        {isPending ? '提交中...' : '提交'}
      </button>
      
      {state?.error && (
        <p style={{ color: '#ef4444', fontSize: '14px' }}>{state.error}</p>
      )}
      {state?.success && (
        <p style={{ color: '#22c55e', fontSize: '14px' }}>{state.message}</p>
      )}
    </form>
  );
}
结果
Loading...

useOptimistic 乐观更新

乐观更新是一种在等待服务器响应时就先更新 UI 的技术,提升用户体验。

实时编辑器
function OptimisticLikeDemo() {
  const [likes, setLikes] = useState(42);
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    likes,
    (currentLikes) => currentLikes + 1
  );

  async function handleLike() {
    addOptimisticLike(null);
    // 模拟 API 调用
    await new Promise(resolve => setTimeout(resolve, 1000));
    setLikes(prev => prev + 1);
  }

  return (
    <div style={{ textAlign: 'center', padding: '20px' }}>
      <button
        onClick={handleLike}
        style={{
          padding: '15px 30px',
          fontSize: '18px',
          background: '#ef4444',
          color: 'white',
          border: 'none',
          borderRadius: '8px',
          cursor: 'pointer'
        }}
      >
        ❤️ {optimisticLikes}
      </button>
      <p style={{ fontSize: '12px', color: '#64748b', marginTop: '10px' }}>
        {optimisticLikes !== likes ? '更新中...' : '点击点赞(乐观更新演示)'}
      </p>
    </div>
  );
}
结果
Loading...

复杂表单模式

动态表单字段

实时编辑器
function DynamicFormFields() {
  const [fields, setFields] = useState([
    { id: 1, name: '', email: '' }
  ]);

  const addField = () => {
    setFields(prev => [
      ...prev,
      { id: Date.now(), name: '', email: '' }
    ]);
  };

  const removeField = (id) => {
    if (fields.length > 1) {
      setFields(prev => prev.filter(f => f.id !== id));
    }
  };

  const updateField = (id, field, value) => {
    setFields(prev => prev.map(f => 
      f.id === id ? { ...f, [field]: value } : f
    ));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    alert(JSON.stringify(fields, null, 2));
  };

  return (
    <form onSubmit={handleSubmit}>
      {fields.map((field, index) => (
        <div key={field.id} style={{ 
          display: 'flex', 
          gap: '10px', 
          marginBottom: '10px',
          padding: '10px',
          background: '#f8fafc',
          borderRadius: '4px'
        }}>
          <span style={{ 
            display: 'flex', 
            alignItems: 'center', 
            width: '24px',
            color: '#64748b'
          }}>
            #{index + 1}
          </span>
          <input
            placeholder="姓名"
            value={field.name}
            onChange={(e) => updateField(field.id, 'name', e.target.value)}
            style={{ padding: '8px', flex: 1 }}
          />
          <input
            placeholder="邮箱"
            value={field.email}
            onChange={(e) => updateField(field.id, 'email', e.target.value)}
            style={{ padding: '8px', flex: 1 }}
          />
          <button
            type="button"
            onClick={() => removeField(field.id)}
            style={{ 
              padding: '8px 12px', 
              background: '#ef4444', 
              color: 'white',
              border: 'none',
              borderRadius: '4px'
            }}
          >
            删除
          </button>
        </div>
      ))}
      <div style={{ display: 'flex', gap: '10px' }}>
        <button
          type="button"
          onClick={addField}
          style={{ 
            padding: '10px', 
            background: '#22c55e', 
            color: 'white',
            border: 'none',
            borderRadius: '4px'
          }}
        >
          + 添加字段
        </button>
        <button
          type="submit"
          style={{ 
            padding: '10px', 
            background: '#3b82f6', 
            color: 'white',
            border: 'none',
            borderRadius: '4px'
          }}
        >
          提交表单
        </button>
      </div>
    </form>
  );
}
结果
Loading...

多步骤表单

实时编辑器
function MultiStepForm() {
  const [step, setStep] = useState(1);
  const [formData, setFormData] = useState({
    // 步骤 1: 个人信息
    firstName: '',
    lastName: '',
    // 步骤 2: 联系方式
    email: '',
    phone: '',
    // 步骤 3: 地址
    address: '',
    city: ''
  });

  const handleChange = (e) => {
    setFormData(prev => ({
      ...prev,
      [e.target.name]: e.target.value
    }));
  };

  const nextStep = () => setStep(s => Math.min(s + 1, 3));
  const prevStep = () => setStep(s => Math.max(s - 1, 1));

  const handleSubmit = () => {
    alert('表单提交成功!\n' + JSON.stringify(formData, null, 2));
  };

  const renderStep = () => {
    switch (step) {
      case 1:
        return (
          <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
            <h4>步骤 1: 个人信息</h4>
            <input
              name="firstName"
              placeholder=""
              value={formData.firstName}
              onChange={handleChange}
              style={{ padding: '10px' }}
            />
            <input
              name="lastName"
              placeholder=""
              value={formData.lastName}
              onChange={handleChange}
              style={{ padding: '10px' }}
            />
          </div>
        );
      case 2:
        return (
          <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
            <h4>步骤 2: 联系方式</h4>
            <input
              name="email"
              type="email"
              placeholder="邮箱"
              value={formData.email}
              onChange={handleChange}
              style={{ padding: '10px' }}
            />
            <input
              name="phone"
              placeholder="电话"
              value={formData.phone}
              onChange={handleChange}
              style={{ padding: '10px' }}
            />
          </div>
        );
      case 3:
        return (
          <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
            <h4>步骤 3: 地址</h4>
            <input
              name="address"
              placeholder="详细地址"
              value={formData.address}
              onChange={handleChange}
              style={{ padding: '10px' }}
            />
            <input
              name="city"
              placeholder="城市"
              value={formData.city}
              onChange={handleChange}
              style={{ padding: '10px' }}
            />
          </div>
        );
    }
  };

  return (
    <div>
      {/* 进度指示器 */}
      <div style={{ display: 'flex', marginBottom: '20px' }}>
        {[1, 2, 3].map(s => (
          <div
            key={s}
            style={{
              flex: 1,
              padding: '10px',
              textAlign: 'center',
              background: s <= step ? '#3b82f6' : '#e2e8f0',
              color: s <= step ? 'white' : '#64748b',
              marginRight: s < 3 ? '2px' : 0
            }}
          >
            {s}
          </div>
        ))}
      </div>

      {/* 表单内容 */}
      {renderStep()}

      {/* 导航按钮 */}
      <div style={{ marginTop: '20px', display: 'flex', gap: '10px' }}>
        {step > 1 && (
          <button
            type="button"
            onClick={prevStep}
            style={{ padding: '10px 20px' }}
          >
            上一步
          </button>
        )}
        {step < 3 ? (
          <button
            type="button"
            onClick={nextStep}
            style={{ padding: '10px 20px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px' }}
          >
            下一步
          </button>
        ) : (
          <button
            type="button"
            onClick={handleSubmit}
            style={{ padding: '10px 20px', background: '#22c55e', color: 'white', border: 'none', borderRadius: '4px' }}
          >
            提交
          </button>
        )}
      </div>
    </div>
  );
}
结果
Loading...

表单库推荐

对于复杂的表单需求,推荐使用成熟的表单库:

react-hook-form

react-hook-form 是一个高性能、灵活的表单库,具有以下特点:

  • 减少不必要的重新渲染
  • 最小化的重新渲染
  • 内置验证支持
  • 与 UI 库良好集成
import { useForm } from 'react-hook-form';

function MyForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting }
} = useForm();

const onSubmit = async (data) => {
await submitForm(data);
};

return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('email', {
required: '邮箱必填',
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: '邮箱格式不正确'
}
})}
/>
{errors.email && <span>{errors.email.message}</span>}

<button type="submit" disabled={isSubmitting}>
提交
</button>
</form>
);
}

小结

  1. 受控组件:表单数据由 React state 控制,适合复杂表单和实时验证
  2. 非受控组件:使用 ref 获取表单值,适合简单场景和文件上传
  3. 表单验证:实时验证、提交验证和密码强度检查
  4. React 19 Actions:useActionState 和 useOptimistic 简化表单处理
  5. 复杂表单:动态字段、多步骤表单等高级模式
  6. 表单库:react-hook-form 等库可以简化复杂表单开发

练习

  1. 实现一个包含实时验证的登录表单
  2. 创建一个多步骤注册表单,包含进度指示
  3. 实现一个动态添加/删除字段的表单
  4. 使用 React 19 的 useActionState 重构一个现有表单
  5. 实现一个带有乐观更新的评论提交表单

参考资源