Skip to content

多个表单元素处理

学习目标

通过本章学习,你将全面掌握:

  • 管理多个表单元素的最佳策略
  • 通用的表单处理模式
  • 动态表单字段的实现
  • 表单数组字段的管理
  • 复杂表单验证系统
  • 性能优化技巧
  • 表单状态管理方案
  • React 19的表单处理新特性

第一部分:多字段表单管理

1.1 对象State管理

jsx
function MultiFieldForm() {
  const [formData, setFormData] = useState({
    // 基本信息
    username: '',
    email: '',
    password: '',
    confirmPassword: '',
    
    // 个人信息
    firstName: '',
    lastName: '',
    age: 0,
    gender: '',
    birthday: '',
    
    // 联系信息
    phone: '',
    address: '',
    city: '',
    country: '',
    zipCode: '',
    
    // 偏好设置
    newsletter: false,
    notifications: false,
    theme: 'light',
    language: 'zh-CN'
  });
  
  // 通用的字段更新处理器
  const handleChange = (field) => (e) => {
    const value = e.target.type === 'checkbox' 
      ? e.target.checked 
      : e.target.value;
    
    setFormData(prev => ({
      ...prev,
      [field]: value
    }));
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('表单数据:', formData);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <section>
        <h3>基本信息</h3>
        <input
          value={formData.username}
          onChange={handleChange('username')}
          placeholder="用户名"
        />
        <input
          type="email"
          value={formData.email}
          onChange={handleChange('email')}
          placeholder="邮箱"
        />
        <input
          type="password"
          value={formData.password}
          onChange={handleChange('password')}
          placeholder="密码"
        />
        <input
          type="password"
          value={formData.confirmPassword}
          onChange={handleChange('confirmPassword')}
          placeholder="确认密码"
        />
      </section>
      
      <section>
        <h3>个人信息</h3>
        <input
          value={formData.firstName}
          onChange={handleChange('firstName')}
          placeholder="名"
        />
        <input
          value={formData.lastName}
          onChange={handleChange('lastName')}
          placeholder="姓"
        />
        <input
          type="number"
          value={formData.age}
          onChange={handleChange('age')}
          placeholder="年龄"
        />
        <select value={formData.gender} onChange={handleChange('gender')}>
          <option value="">选择性别</option>
          <option value="male">男</option>
          <option value="female">女</option>
          <option value="other">其他</option>
        </select>
        <input
          type="date"
          value={formData.birthday}
          onChange={handleChange('birthday')}
        />
      </section>
      
      <section>
        <h3>联系信息</h3>
        <input
          type="tel"
          value={formData.phone}
          onChange={handleChange('phone')}
          placeholder="电话"
        />
        <input
          value={formData.address}
          onChange={handleChange('address')}
          placeholder="地址"
        />
        <input
          value={formData.city}
          onChange={handleChange('city')}
          placeholder="城市"
        />
        <input
          value={formData.country}
          onChange={handleChange('country')}
          placeholder="国家"
        />
        <input
          value={formData.zipCode}
          onChange={handleChange('zipCode')}
          placeholder="邮编"
        />
      </section>
      
      <section>
        <h3>偏好设置</h3>
        <label>
          <input
            type="checkbox"
            checked={formData.newsletter}
            onChange={handleChange('newsletter')}
          />
          订阅Newsletter
        </label>
        
        <label>
          <input
            type="checkbox"
            checked={formData.notifications}
            onChange={handleChange('notifications')}
          />
          接收通知
        </label>
        
        <select value={formData.theme} onChange={handleChange('theme')}>
          <option value="light">浅色主题</option>
          <option value="dark">深色主题</option>
        </select>
        
        <select value={formData.language} onChange={handleChange('language')}>
          <option value="zh-CN">简体中文</option>
          <option value="en-US">English</option>
        </select>
      </section>
      
      <button type="submit">提交</button>
    </form>
  );
}

1.2 通用处理函数

jsx
function UnifiedHandlerForm() {
  const [formData, setFormData] = useState({
    text: '',
    number: 0,
    email: '',
    password: '',
    textarea: '',
    select: '',
    checkbox: false,
    radio: '',
    date: '',
    time: '',
    color: '#000000',
    range: 50
  });
  
  // 通用的change处理器
  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    
    setFormData(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value
    }));
  };
  
  return (
    <form>
      <input
        name="text"
        value={formData.text}
        onChange={handleChange}
        placeholder="文本"
      />
      
      <input
        name="number"
        type="number"
        value={formData.number}
        onChange={handleChange}
      />
      
      <input
        name="email"
        type="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="邮箱"
      />
      
      <input
        name="password"
        type="password"
        value={formData.password}
        onChange={handleChange}
        placeholder="密码"
      />
      
      <textarea
        name="textarea"
        value={formData.textarea}
        onChange={handleChange}
        placeholder="多行文本"
      />
      
      <select name="select" value={formData.select} onChange={handleChange}>
        <option value="">选择</option>
        <option value="a">选项A</option>
        <option value="b">选项B</option>
      </select>
      
      <label>
        <input
          name="checkbox"
          type="checkbox"
          checked={formData.checkbox}
          onChange={handleChange}
        />
        复选框
      </label>
      
      <div>
        <label>
          <input
            name="radio"
            type="radio"
            value="option1"
            checked={formData.radio === 'option1'}
            onChange={handleChange}
          />
          选项1
        </label>
        <label>
          <input
            name="radio"
            type="radio"
            value="option2"
            checked={formData.radio === 'option2'}
            onChange={handleChange}
          />
          选项2
        </label>
      </div>
      
      <input
        name="date"
        type="date"
        value={formData.date}
        onChange={handleChange}
      />
      
      <input
        name="time"
        type="time"
        value={formData.time}
        onChange={handleChange}
      />
      
      <input
        name="color"
        type="color"
        value={formData.color}
        onChange={handleChange}
      />
      
      <div>
        <label>范围 ({formData.range}):</label>
        <input
          name="range"
          type="range"
          value={formData.range}
          onChange={handleChange}
          min="0"
          max="100"
        />
      </div>
    </form>
  );
}

1.3 分组管理State

jsx
function GroupedStateForm() {
  // 按功能分组
  const [basicInfo, setBasicInfo] = useState({
    username: '',
    email: '',
    password: ''
  });
  
  const [personalInfo, setPersonalInfo] = useState({
    firstName: '',
    lastName: '',
    age: 0,
    gender: ''
  });
  
  const [contactInfo, setContactInfo] = useState({
    phone: '',
    address: '',
    city: ''
  });
  
  const [preferences, setPreferences] = useState({
    newsletter: false,
    notifications: true,
    theme: 'light'
  });
  
  // 为每个组创建处理器
  const handleBasicChange = (field) => (e) => {
    setBasicInfo(prev => ({
      ...prev,
      [field]: e.target.value
    }));
  };
  
  const handlePersonalChange = (field) => (e) => {
    setPersonalInfo(prev => ({
      ...prev,
      [field]: e.target.value
    }));
  };
  
  const handleContactChange = (field) => (e) => {
    setContactInfo(prev => ({
      ...prev,
      [field]: e.target.value
    }));
  };
  
  const handlePreferenceChange = (field) => (e) => {
    const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
    setPreferences(prev => ({
      ...prev,
      [field]: value
    }));
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    const allData = {
      ...basicInfo,
      ...personalInfo,
      ...contactInfo,
      ...preferences
    };
    
    console.log('提交:', allData);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      {/* 每个组独立管理,只有相关字段变化时才重新渲染 */}
      <BasicInfoSection data={basicInfo} onChange={handleBasicChange} />
      <PersonalInfoSection data={personalInfo} onChange={handlePersonalChange} />
      <ContactInfoSection data={contactInfo} onChange={handleContactChange} />
      <PreferencesSection data={preferences} onChange={handlePreferenceChange} />
      
      <button type="submit">提交</button>
    </form>
  );
}

// 拆分的子组件
const BasicInfoSection = React.memo(({ data, onChange }) => {
  console.log('BasicInfoSection渲染');
  
  return (
    <section>
      <h3>基本信息</h3>
      <input value={data.username} onChange={onChange('username')} placeholder="用户名" />
      <input value={data.email} onChange={onChange('email')} placeholder="邮箱" />
      <input type="password" value={data.password} onChange={onChange('password')} placeholder="密码" />
    </section>
  );
});

第二部分:动态表单字段

2.1 动态添加/删除字段

jsx
function DynamicFields() {
  const [fields, setFields] = useState([
    { id: 1, value: '', label: '字段1' }
  ]);
  
  const addField = () => {
    const newId = Math.max(...fields.map(f => f.id), 0) + 1;
    setFields(prev => [
      ...prev,
      { id: newId, value: '', label: `字段${newId}` }
    ]);
  };
  
  const removeField = (id) => {
    if (fields.length > 1) {
      setFields(prev => prev.filter(field => field.id !== id));
    }
  };
  
  const updateField = (id, value) => {
    setFields(prev => prev.map(field =>
      field.id === id ? { ...field, value } : field
    ));
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('所有字段:', fields);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <h3>动态表单字段</h3>
      
      {fields.map(field => (
        <div key={field.id} className="dynamic-field">
          <label>{field.label}:</label>
          <input
            value={field.value}
            onChange={e => updateField(field.id, e.target.value)}
            placeholder={`输入${field.label}`}
          />
          <button
            type="button"
            onClick={() => removeField(field.id)}
            disabled={fields.length === 1}
          >
            删除
          </button>
        </div>
      ))}
      
      <button type="button" onClick={addField}>
        添加字段
      </button>
      
      <button type="submit">提交</button>
    </form>
  );
}

2.2 表单数组(联系人列表)

jsx
function ContactsForm() {
  const [contacts, setContacts] = useState([
    { id: 1, name: '', phone: '', email: '', type: 'personal' }
  ]);
  
  const addContact = () => {
    const newId = Math.max(...contacts.map(c => c.id), 0) + 1;
    setContacts(prev => [
      ...prev,
      { id: newId, name: '', phone: '', email: '', type: 'personal' }
    ]);
  };
  
  const removeContact = (id) => {
    if (contacts.length > 1) {
      setContacts(prev => prev.filter(c => c.id !== id));
    }
  };
  
  const updateContact = (id, field, value) => {
    setContacts(prev => prev.map(contact =>
      contact.id === id
        ? { ...contact, [field]: value }
        : contact
    ));
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('联系人列表:', contacts);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <h3>联系人管理</h3>
      
      {contacts.map((contact, index) => (
        <div key={contact.id} className="contact-card">
          <h4>联系人 {index + 1}</h4>
          
          <input
            value={contact.name}
            onChange={e => updateContact(contact.id, 'name', e.target.value)}
            placeholder="姓名"
          />
          
          <input
            type="tel"
            value={contact.phone}
            onChange={e => updateContact(contact.id, 'phone', e.target.value)}
            placeholder="电话"
          />
          
          <input
            type="email"
            value={contact.email}
            onChange={e => updateContact(contact.id, 'email', e.target.value)}
            placeholder="邮箱"
          />
          
          <select
            value={contact.type}
            onChange={e => updateContact(contact.id, 'type', e.target.value)}
          >
            <option value="personal">个人</option>
            <option value="work">工作</option>
            <option value="emergency">紧急联系人</option>
          </select>
          
          <button
            type="button"
            onClick={() => removeContact(contact.id)}
            disabled={contacts.length === 1}
          >
            删除
          </button>
        </div>
      ))}
      
      <button type="button" onClick={addContact}>
        添加联系人
      </button>
      
      <button type="submit">保存全部</button>
    </form>
  );
}

2.3 嵌套表单数组(订单项)

jsx
function OrderForm() {
  const [orderItems, setOrderItems] = useState([
    {
      id: 1,
      product: '',
      quantity: 1,
      price: 0,
      options: []
    }
  ]);
  
  const [availableOptions, setAvailableOptions] = useState([
    '加急', '包装', '保险', '礼品卡'
  ]);
  
  const addItem = () => {
    const newId = Math.max(...orderItems.map(item => item.id), 0) + 1;
    setOrderItems(prev => [
      ...prev,
      { id: newId, product: '', quantity: 1, price: 0, options: [] }
    ]);
  };
  
  const removeItem = (id) => {
    if (orderItems.length > 1) {
      setOrderItems(prev => prev.filter(item => item.id !== id));
    }
  };
  
  const updateItem = (id, field, value) => {
    setOrderItems(prev => prev.map(item =>
      item.id === id ? { ...item, [field]: value } : item
    ));
  };
  
  const toggleOption = (itemId, option) => {
    setOrderItems(prev => prev.map(item => {
      if (item.id === itemId) {
        const options = item.options.includes(option)
          ? item.options.filter(opt => opt !== option)
          : [...item.options, option];
        return { ...item, options };
      }
      return item;
    }));
  };
  
  const calculateTotal = useMemo(() => {
    return orderItems.reduce((sum, item) => {
      let itemTotal = item.quantity * item.price;
      
      // 选项加价
      if (item.options.includes('加急')) itemTotal += 50;
      if (item.options.includes('包装')) itemTotal += 20;
      if (item.options.includes('保险')) itemTotal += 10;
      
      return sum + itemTotal;
    }, 0);
  }, [orderItems]);
  
  return (
    <form>
      <h3>订单管理</h3>
      
      {orderItems.map((item, index) => (
        <div key={item.id} className="order-item">
          <h4>商品 {index + 1}</h4>
          
          <input
            value={item.product}
            onChange={e => updateItem(item.id, 'product', e.target.value)}
            placeholder="商品名称"
          />
          
          <input
            type="number"
            value={item.quantity}
            onChange={e => updateItem(item.id, 'quantity', Number(e.target.value))}
            min="1"
            placeholder="数量"
          />
          
          <input
            type="number"
            value={item.price}
            onChange={e => updateItem(item.id, 'price', Number(e.target.value))}
            min="0"
            step="0.01"
            placeholder="单价"
          />
          
          <div className="options">
            <p>附加选项:</p>
            {availableOptions.map(option => (
              <label key={option}>
                <input
                  type="checkbox"
                  checked={item.options.includes(option)}
                  onChange={() => toggleOption(item.id, option)}
                />
                {option}
              </label>
            ))}
          </div>
          
          <p className="item-subtotal">
            小计: ¥{(item.quantity * item.price).toFixed(2)}
          </p>
          
          <button
            type="button"
            onClick={() => removeItem(item.id)}
            disabled={orderItems.length === 1}
          >
            删除
          </button>
        </div>
      ))}
      
      <button type="button" onClick={addItem}>
        添加商品
      </button>
      
      <div className="order-total">
        <h3>订单总计: ¥{calculateTotal.toFixed(2)}</h3>
      </div>
    </form>
  );
}

第三部分:表单验证

3.1 字段级验证

jsx
function FieldLevelValidation() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    phone: '',
    age: ''
  });
  
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  
  // 验证规则
  const validators = {
    username: (value) => {
      if (!value) return '用户名不能为空';
      if (value.length < 3) return '用户名至少3个字符';
      if (value.length > 20) return '用户名最多20个字符';
      if (!/^[a-zA-Z0-9_]+$/.test(value)) return '只能包含字母、数字和下划线';
      return '';
    },
    
    email: (value) => {
      if (!value) return '邮箱不能为空';
      if (!/\S+@\S+\.\S+/.test(value)) return '邮箱格式不正确';
      return '';
    },
    
    password: (value) => {
      if (!value) return '密码不能为空';
      if (value.length < 8) return '密码至少8个字符';
      if (!/[A-Z]/.test(value)) return '密码必须包含大写字母';
      if (!/[a-z]/.test(value)) return '密码必须包含小写字母';
      if (!/[0-9]/.test(value)) return '密码必须包含数字';
      if (!/[!@#$%^&*]/.test(value)) return '密码必须包含特殊字符';
      return '';
    },
    
    phone: (value) => {
      if (!value) return '电话不能为空';
      if (!/^1[3-9]\d{9}$/.test(value)) return '请输入正确的手机号';
      return '';
    },
    
    age: (value) => {
      if (!value) return '年龄不能为空';
      const age = Number(value);
      if (age < 0 || age > 120) return '年龄必须在0-120之间';
      return '';
    }
  };
  
  const handleChange = (field) => (e) => {
    const value = e.target.value;
    
    setFormData(prev => ({
      ...prev,
      [field]: value
    }));
    
    // 如果字段已被访问过,实时验证
    if (touched[field]) {
      const error = validators[field](value);
      setErrors(prev => ({
        ...prev,
        [field]: error
      }));
    }
  };
  
  const handleBlur = (field) => () => {
    setTouched(prev => ({
      ...prev,
      [field]: true
    }));
    
    const error = validators[field](formData[field]);
    setErrors(prev => ({
      ...prev,
      [field]: error
    }));
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    // 标记所有字段为已访问
    const allTouched = {};
    Object.keys(formData).forEach(key => {
      allTouched[key] = true;
    });
    setTouched(allTouched);
    
    // 验证所有字段
    const allErrors = {};
    Object.keys(formData).forEach(field => {
      const error = validators[field](formData[field]);
      if (error) {
        allErrors[field] = error;
      }
    });
    setErrors(allErrors);
    
    // 检查是否有错误
    if (Object.keys(allErrors).length === 0) {
      console.log('提交数据:', formData);
      alert('提交成功!');
    } else {
      alert('请检查表单错误');
    }
  };
  
  const isFormValid = Object.keys(formData).every(field => 
    touched[field] && !errors[field]
  );
  
  return (
    <form onSubmit={handleSubmit} className="validated-form">
      {Object.keys(formData).map(field => (
        <div key={field} className="form-field">
          <label>{field}:</label>
          <input
            type={field === 'password' ? 'password' : field === 'email' ? 'email' : 'text'}
            value={formData[field]}
            onChange={handleChange(field)}
            onBlur={handleBlur(field)}
            className={touched[field] && errors[field] ? 'error' : ''}
          />
          {touched[field] && errors[field] && (
            <span className="error-message">{errors[field]}</span>
          )}
          {touched[field] && !errors[field] && formData[field] && (
            <span className="success-icon">✓</span>
          )}
        </div>
      ))}
      
      <button type="submit" disabled={!isFormValid}>
        提交
      </button>
    </form>
  );
}

3.2 表单级验证

jsx
function FormLevelValidation() {
  const [formData, setFormData] = useState({
    startDate: '',
    endDate: '',
    minValue: 0,
    maxValue: 100,
    password: '',
    confirmPassword: ''
  });
  
  const [errors, setErrors] = useState({});
  
  // 跨字段验证
  const validateForm = () => {
    const newErrors = {};
    
    // 验证日期范围
    if (formData.startDate && formData.endDate) {
      if (new Date(formData.startDate) > new Date(formData.endDate)) {
        newErrors.dateRange = '开始日期不能晚于结束日期';
      }
    }
    
    // 验证数值范围
    if (formData.minValue >= formData.maxValue) {
      newErrors.valueRange = '最小值必须小于最大值';
    }
    
    // 验证密码匹配
    if (formData.password !== formData.confirmPassword) {
      newErrors.passwordMatch = '两次密码不一致';
    }
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
  
  const handleChange = (field) => (e) => {
    setFormData(prev => ({
      ...prev,
      [field]: e.target.value
    }));
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    if (validateForm()) {
      console.log('提交:', formData);
      alert('提交成功!');
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <h4>日期范围:</h4>
        <input
          type="date"
          value={formData.startDate}
          onChange={handleChange('startDate')}
        />
        <span>至</span>
        <input
          type="date"
          value={formData.endDate}
          onChange={handleChange('endDate')}
        />
        {errors.dateRange && (
          <span className="error">{errors.dateRange}</span>
        )}
      </div>
      
      <div>
        <h4>数值范围:</h4>
        <input
          type="number"
          value={formData.minValue}
          onChange={handleChange('minValue')}
          placeholder="最小值"
        />
        <span>-</span>
        <input
          type="number"
          value={formData.maxValue}
          onChange={handleChange('maxValue')}
          placeholder="最大值"
        />
        {errors.valueRange && (
          <span className="error">{errors.valueRange}</span>
        )}
      </div>
      
      <div>
        <h4>密码:</h4>
        <input
          type="password"
          value={formData.password}
          onChange={handleChange('password')}
          placeholder="密码"
        />
        <input
          type="password"
          value={formData.confirmPassword}
          onChange={handleChange('confirmPassword')}
          placeholder="确认密码"
        />
        {errors.passwordMatch && (
          <span className="error">{errors.passwordMatch}</span>
        )}
      </div>
      
      <button type="submit">提交</button>
    </form>
  );
}

第四部分:性能优化

4.1 字段组件化

jsx
// 拆分为独立组件避免整体重新渲染
const FormField = React.memo(function FormField({ 
  label, 
  name,
  type = 'text',
  value, 
  onChange, 
  onBlur,
  error,
  touched,
  placeholder 
}) {
  console.log('FormField渲染:', label);
  
  return (
    <div className="form-field">
      <label>{label}</label>
      <input
        type={type}
        name={name}
        value={value}
        onChange={onChange}
        onBlur={onBlur}
        placeholder={placeholder}
        className={touched && error ? 'error' : ''}
      />
      {touched && error && (
        <span className="error-message">{error}</span>
      )}
    </div>
  );
});

function OptimizedForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    phone: '',
    address: ''
  });
  
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  
  // 使用useCallback缓存处理器
  const handleChange = useCallback((field) => (e) => {
    setFormData(prev => ({
      ...prev,
      [field]: e.target.value
    }));
  }, []);
  
  const handleBlur = useCallback((field) => () => {
    setTouched(prev => ({
      ...prev,
      [field]: true
    }));
  }, []);
  
  return (
    <form>
      {/* 每个字段只在自己变化时重新渲染 */}
      <FormField
        label="用户名"
        name="username"
        value={formData.username}
        onChange={handleChange('username')}
        onBlur={handleBlur('username')}
        error={errors.username}
        touched={touched.username}
      />
      
      <FormField
        label="邮箱"
        name="email"
        type="email"
        value={formData.email}
        onChange={handleChange('email')}
        onBlur={handleBlur('email')}
        error={errors.email}
        touched={touched.email}
      />
      
      {/* 其他字段... */}
    </form>
  );
}

4.2 防抖输入

jsx
function DebouncedForm() {
  const [immediate, setImmediate] = useState('');
  const [debounced, setDebounced] = useState('');
  
  // 防抖搜索
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebounced(immediate);
    }, 500);
    
    return () => clearTimeout(timer);
  }, [immediate]);
  
  // 执行搜索
  useEffect(() => {
    if (debounced) {
      console.log('搜索:', debounced);
      performSearch(debounced);
    }
  }, [debounced]);
  
  return (
    <div>
      <input
        value={immediate}
        onChange={e => setImmediate(e.target.value)}
        placeholder="输入搜索..."
      />
      <p>即时值: {immediate}</p>
      <p>防抖值: {debounced}</p>
    </div>
  );
}

// 自定义防抖Hook
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    
    return () => clearTimeout(timer);
  }, [value, delay]);
  
  return debouncedValue;
}

// 使用防抖Hook
function SearchForm() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedTerm = useDebounce(searchTerm, 500);
  
  useEffect(() => {
    if (debouncedTerm) {
      console.log('执行搜索:', debouncedTerm);
    }
  }, [debouncedTerm]);
  
  return (
    <input
      value={searchTerm}
      onChange={e => setSearchTerm(e.target.value)}
      placeholder="搜索..."
    />
  );
}

第五部分:复杂表单管理

5.1 向导式表单(多步骤)

jsx
function WizardForm() {
  const [step, setStep] = useState(1);
  const [formData, setFormData] = useState({
    // Step 1
    username: '',
    email: '',
    password: '',
    
    // Step 2
    firstName: '',
    lastName: '',
    phone: '',
    
    // Step 3
    address: '',
    city: '',
    zipCode: '',
    
    // Step 4
    cardNumber: '',
    expiryDate: '',
    cvv: ''
  });
  
  const updateFormData = (updates) => {
    setFormData(prev => ({
      ...prev,
      ...updates
    }));
  };
  
  const nextStep = () => {
    if (validateStep(step)) {
      setStep(s => Math.min(s + 1, 4));
    }
  };
  
  const prevStep = () => {
    setStep(s => Math.max(s - 1, 1));
  };
  
  const validateStep = (stepNumber) => {
    // 根据步骤验证不同字段
    switch (stepNumber) {
      case 1:
        return formData.username && formData.email && formData.password;
      case 2:
        return formData.firstName && formData.lastName && formData.phone;
      case 3:
        return formData.address && formData.city && formData.zipCode;
      case 4:
        return formData.cardNumber && formData.expiryDate && formData.cvv;
      default:
        return true;
    }
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    if (step === 4 && validateStep(4)) {
      console.log('提交完整数据:', formData);
      alert('注册成功!');
    }
  };
  
  const renderStep = () => {
    switch (step) {
      case 1:
        return <AccountStep data={formData} onChange={updateFormData} />;
      case 2:
        return <PersonalStep data={formData} onChange={updateFormData} />;
      case 3:
        return <AddressStep data={formData} onChange={updateFormData} />;
      case 4:
        return <PaymentStep data={formData} onChange={updateFormData} />;
      default:
        return null;
    }
  };
  
  return (
    <div className="wizard-form">
      <div className="progress-steps">
        {[1, 2, 3, 4].map(s => (
          <div key={s} className={`step ${step >= s ? 'active' : ''}`}>
            {s}. {['账号', '个人', '地址', '支付'][s - 1]}
          </div>
        ))}
      </div>
      
      <form onSubmit={handleSubmit}>
        {renderStep()}
        
        <div className="form-navigation">
          {step > 1 && (
            <button type="button" onClick={prevStep}>
              上一步
            </button>
          )}
          
          {step < 4 ? (
            <button type="button" onClick={nextStep}>
              下一步
            </button>
          ) : (
            <button type="submit">
              提交
            </button>
          )}
        </div>
      </form>
    </div>
  );
}

// 步骤组件
function AccountStep({ data, onChange }) {
  return (
    <div>
      <h3>账号信息</h3>
      <input
        value={data.username}
        onChange={e => onChange({ username: e.target.value })}
        placeholder="用户名"
      />
      <input
        type="email"
        value={data.email}
        onChange={e => onChange({ email: e.target.value })}
        placeholder="邮箱"
      />
      <input
        type="password"
        value={data.password}
        onChange={e => onChange({ password: e.target.value })}
        placeholder="密码"
      />
    </div>
  );
}

5.2 条件表单字段

jsx
function ConditionalFields() {
  const [formData, setFormData] = useState({
    userType: 'individual',  // individual or company
    name: '',
    companyName: '',
    taxId: '',
    email: '',
    phone: ''
  });
  
  const handleChange = (field) => (e) => {
    setFormData(prev => ({
      ...prev,
      [field]: e.target.value
    }));
  };
  
  const isCompany = formData.userType === 'company';
  
  return (
    <form>
      <div>
        <label>用户类型:</label>
        <select value={formData.userType} onChange={handleChange('userType')}>
          <option value="individual">个人</option>
          <option value="company">企业</option>
        </select>
      </div>
      
      {!isCompany && (
        <div>
          <label>姓名:</label>
          <input
            value={formData.name}
            onChange={handleChange('name')}
            placeholder="请输入姓名"
          />
        </div>
      )}
      
      {isCompany && (
        <>
          <div>
            <label>公司名称:</label>
            <input
              value={formData.companyName}
              onChange={handleChange('companyName')}
              placeholder="请输入公司名称"
            />
          </div>
          
          <div>
            <label>税号:</label>
            <input
              value={formData.taxId}
              onChange={handleChange('taxId')}
              placeholder="请输入税号"
            />
          </div>
        </>
      )}
      
      <div>
        <label>邮箱:</label>
        <input
          type="email"
          value={formData.email}
          onChange={handleChange('email')}
          placeholder="邮箱"
        />
      </div>
      
      <div>
        <label>电话:</label>
        <input
          type="tel"
          value={formData.phone}
          onChange={handleChange('phone')}
          placeholder="电话"
        />
      </div>
      
      <button type="submit">提交</button>
    </form>
  );
}

第六部分:实战案例

6.1 完整的注册表单

jsx
function RegistrationForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    confirmPassword: '',
    firstName: '',
    lastName: '',
    birthday: '',
    gender: '',
    country: '',
    agreeToTerms: false
  });
  
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  const [submitting, setSubmitting] = useState(false);
  
  const handleChange = useCallback((field) => (e) => {
    const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
    
    setFormData(prev => ({
      ...prev,
      [field]: value
    }));
    
    // 清除该字段的错误
    if (errors[field]) {
      setErrors(prev => {
        const newErrors = { ...prev };
        delete newErrors[field];
        return newErrors;
      });
    }
  }, [errors]);
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    // 验证
    const newErrors = validateForm(formData);
    setErrors(newErrors);
    
    if (Object.keys(newErrors).length > 0) {
      return;
    }
    
    setSubmitting(true);
    
    try {
      const response = await fetch('/api/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData)
      });
      
      if (response.ok) {
        alert('注册成功!');
      } else {
        const data = await response.json();
        setErrors({ submit: data.message });
      }
    } catch (error) {
      setErrors({ submit: '注册失败,请重试' });
    } finally {
      setSubmitting(false);
    }
  };
  
  return (
    <form onSubmit={handleSubmit} className="registration-form">
      <h2>注册账号</h2>
      
      {errors.submit && (
        <div className="error-banner">{errors.submit}</div>
      )}
      
      <section>
        <h3>账号信息</h3>
        <FormField
          label="用户名"
          value={formData.username}
          onChange={handleChange('username')}
          error={errors.username}
        />
        <FormField
          label="邮箱"
          type="email"
          value={formData.email}
          onChange={handleChange('email')}
          error={errors.email}
        />
        <FormField
          label="密码"
          type="password"
          value={formData.password}
          onChange={handleChange('password')}
          error={errors.password}
        />
        <FormField
          label="确认密码"
          type="password"
          value={formData.confirmPassword}
          onChange={handleChange('confirmPassword')}
          error={errors.confirmPassword}
        />
      </section>
      
      <button type="submit" disabled={submitting || !formData.agreeToTerms}>
        {submitting ? '提交中...' : '注册'}
      </button>
    </form>
  );
}

function validateForm(data) {
  const errors = {};
  
  if (!data.username) errors.username = '用户名不能为空';
  if (!data.email) errors.email = '邮箱不能为空';
  if (!data.password) errors.password = '密码不能为空';
  if (data.password !== data.confirmPassword) errors.confirmPassword = '密码不一致';
  if (!data.agreeToTerms) errors.agreeToTerms = '必须同意条款';
  
  return errors;
}

练习题

基础练习

  1. 创建一个包含10个字段的表单
  2. 实现通用的change处理器
  3. 添加表单验证

进阶练习

  1. 实现动态表单字段添加/删除
  2. 创建表单数组管理(如联系人列表)
  3. 优化大型表单性能

高级练习

  1. 实现复杂的表单验证系统
  2. 创建表单构建器
  3. 使用React 19特性优化表单

通过本章学习,你已经掌握了多个表单元素的处理技巧。这是构建复杂表单应用的基础。继续学习,成为表单大师!