React表单处理

表单是Web应用中用户交互的核心部分。React提供了一套完整的表单处理机制,包括受控组件、表单验证、动态表单等特性。

React表单处理的两种方式

受控组件 vs 非受控组件

受控组件

表单数据由React状态管理

  • 通过state存储表单值
  • 通过onChange更新state
  • React完全控制表单状态
  • 推荐使用的方式
非受控组件

表单数据由DOM自身管理

  • 通过ref获取表单值
  • 类似传统HTML表单
  • 表单状态在DOM中
  • 某些场景下使用

受控组件(Controlled Components)

在React中,受控组件是指表单元素的值由React的state控制,并通过事件处理器更新的组件。

受控组件示例


import React, { useState } from 'react';

function ControlledForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    rememberMe: false
  });

  // 处理输入变化
  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setFormData({
      ...formData,
      [name]: type === 'checkbox' ? checked : value
    });
  };

  // 处理表单提交
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('表单数据:', formData);
    // 这里可以发送数据到服务器
  };

  return (
    <form onSubmit={handleSubmit} className="controlled-form">
      <div className="form-group">
        <label htmlFor="username">用户名:</label>
        <input
          type="text"
          id="username"
          name="username"
          value={formData.username}
          onChange={handleChange}
          placeholder="请输入用户名"
        />
      </div>

      <div className="form-group">
        <label htmlFor="email">邮箱:</label>
        <input
          type="email"
          id="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
          placeholder="请输入邮箱"
        />
      </div>

      <div className="form-group">
        <label htmlFor="password">密码:</label>
        <input
          type="password"
          id="password"
          name="password"
          value={formData.password}
          onChange={handleChange}
          placeholder="请输入密码"
        />
      </div>

      <div className="form-group">
        <label>
          <input
            type="checkbox"
            name="rememberMe"
            checked={formData.rememberMe}
            onChange={handleChange}
          />
          记住我
        </label>
      </div>

      <button type="submit">提交</button>
      <button type="button" onClick={() => setFormData({
        username: '',
        email: '',
        password: '',
        rememberMe: false
      })}>
        重置
      </button>
    </form>
  );
}
                        

不同表单元素的受控处理


function AllFormElements() {
  const [formState, setFormState] = useState({
    // 文本输入
    textInput: '',
    emailInput: '',
    passwordInput: '',
    textareaInput: '',

    // 选择框
    selectInput: 'option1',
    multiSelect: ['option1'],

    // 单选和多选
    radioInput: 'male',
    checkboxGroup: {
      option1: false,
      option2: true,
      option3: false
    },

    // 其他
    rangeInput: 50,
    dateInput: '',
    colorInput: '#61dafb'
  });

  // 通用变化处理器
  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;

    if (type === 'checkbox' && name in formState.checkboxGroup) {
      // 处理checkbox group
      setFormState({
        ...formState,
        checkboxGroup: {
          ...formState.checkboxGroup,
          [name]: checked
        }
      });
    } else if (type === 'checkbox') {
      // 处理单个checkbox
      setFormState({
        ...formState,
        [name]: checked
      });
    } else if (type === 'select-multiple') {
      // 处理多选select
      const selectedOptions = Array.from(e.target.selectedOptions).map(
        option => option.value
      );
      setFormState({
        ...formState,
        [name]: selectedOptions
      });
    } else {
      // 处理其他输入类型
      setFormState({
        ...formState,
        [name]: value
      });
    }
  };

  return (
    <form>
      {/* 文本输入 */}
      <input
        type="text"
        name="textInput"
        value={formState.textInput}
        onChange={handleChange}
        placeholder="文本输入"
      />

      {/* 邮箱输入 */}
      <input
        type="email"
        name="emailInput"
        value={formState.emailInput}
        onChange={handleChange}
        placeholder="邮箱"
      />

      {/* 密码输入 */}
      <input
        type="password"
        name="passwordInput"
        value={formState.passwordInput}
        onChange={handleChange}
        placeholder="密码"
      />

      {/* 文本区域 */}
      <textarea
        name="textareaInput"
        value={formState.textareaInput}
        onChange={handleChange}
        placeholder="多行文本"
      />

      {/* 下拉选择 */}
      <select
        name="selectInput"
        value={formState.selectInput}
        onChange={handleChange}
      >
        <option value="option1">选项1</option>
        <option value="option2">选项2</option>
        <option value="option3">选项3</option>
      </select>

      {/* 多选下拉 */}
      <select
        name="multiSelect"
        value={formState.multiSelect}
        onChange={handleChange}
        multiple
      >
        <option value="option1">选项1</option>
        <option value="option2">选项2</option>
        <option value="option3">选项3</option>
      </select>

      {/* 单选按钮组 */}
      <div>
        <label>
          <input
            type="radio"
            name="radioInput"
            value="male"
            checked={formState.radioInput === 'male'}
            onChange={handleChange}
          />
          男
        </label>
        <label>
          <input
            type="radio"
            name="radioInput"
            value="female"
            checked={formState.radioInput === 'female'}
            onChange={handleChange}
          />
          女
        </label>
      </div>

      {/* 复选框组 */}
      <div>
        {['option1', 'option2', 'option3'].map(option => (
          <label key={option}>
            <input
              type="checkbox"
              name={option}
              checked={formState.checkboxGroup[option]}
              onChange={handleChange}
            />
            {option}
          </label>
        ))}
      </div>

      {/* 范围选择器 */}
      <input
        type="range"
        name="rangeInput"
        min="0"
        max="100"
        value={formState.rangeInput}
        onChange={handleChange}
      />

      {/* 日期选择器 */}
      <input
        type="date"
        name="dateInput"
        value={formState.dateInput}
        onChange={handleChange}
      />

      {/* 颜色选择器 */}
      <input
        type="color"
        name="colorInput"
        value={formState.colorInput}
        onChange={handleChange}
      />
    </form>
  );
}
                    

非受控组件(Uncontrolled Components)

非受控组件使用ref直接从DOM获取表单值,而不是通过state控制。

非受控组件示例


import React, { useRef } from 'react';

function UncontrolledForm() {
  const formRef = useRef(null);
  const usernameRef = useRef(null);
  const emailRef = useRef(null);

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

    // 方式1:通过ref获取单个输入的值
    console.log('用户名:', usernameRef.current.value);
    console.log('邮箱:', emailRef.current.value);

    // 方式2:通过FormData获取所有表单数据
    const formData = new FormData(formRef.current);
    const data = Object.fromEntries(formData.entries());
    console.log('所有表单数据:', data);

    // 方式3:遍历表单元素
    const elements = formRef.current.elements;
    const values = {};
    for (let i = 0; i < elements.length; i++) {
      const element = elements[i];
      if (element.name) {
        if (element.type === 'checkbox') {
          values[element.name] = element.checked;
        } else if (element.type === 'radio') {
          if (element.checked) {
            values[element.name] = element.value;
          }
        } else {
          values[element.name] = element.value;
        }
      }
    }
    console.log('遍历获取的值:', values);
  };

  // 设置默认值
  const setDefaultValues = () => {
    usernameRef.current.value = '默认用户';
    emailRef.current.value = 'default@example.com';
  };

  return (
    <form ref={formRef} onSubmit={handleSubmit} className="uncontrolled-form">
      <div className="form-group">
        <label htmlFor="uname">用户名:</label>
        <input
          type="text"
          id="uname"
          name="username"
          ref={usernameRef}
          placeholder="请输入用户名"
          defaultValue="访客"  // 使用defaultValue而不是value
        />
      </div>

      <div className="form-group">
        <label htmlFor="uemail">邮箱:</label>
        <input
          type="email"
          id="uemail"
          name="email"
          ref={emailRef}
          placeholder="请输入邮箱"
        />
      </div>

      <div className="form-group">
        <label>
          <input
            type="checkbox"
            name="rememberMe"
            defaultChecked={true}  // 使用defaultChecked而不是checked
          />
          记住我
        </label>
      </div>

      <div className="form-group">
        <label>性别:</label>
        <label>
          <input
            type="radio"
            name="gender"
            value="male"
            defaultChecked  // 使用defaultChecked而不是checked
          />
          男
        </label>
        <label>
          <input
            type="radio"
            name="gender"
            value="female"
          />
          女
        </label>
      </div>

      <button type="submit">提交</button>
      <button type="button" onClick={setDefaultValues}>
        设置默认值
      </button>
    </form>
  );
}
                        
特性 受控组件 非受控组件
状态管理 React状态控制 DOM自身管理
值获取 通过state获取 通过ref或FormData获取
值设置 通过state设置 通过defaultValue/defaultChecked设置
实时验证 容易实现 较难实现
性能 每次输入都触发渲染 只在需要时获取值
代码复杂度 较高 较低
适用场景 需要实时验证、即时反馈 简单表单、文件上传、第三方库集成
非受控组件的注意事项
  • 使用defaultValue而不是value设置初始值
  • 使用defaultChecked而不是checked设置复选框和单选按钮
  • 文件上传(input type="file")总是非受控组件
  • 非受控组件难以实现实时验证
  • 可能会与某些React特性(如条件渲染)产生冲突

表单验证

表单验证是确保用户输入符合要求的重要环节。React中可以轻松实现各种验证逻辑。

实时表单验证演示

// 表单验证代码将在这里显示

React表单验证实现


import React, { useState } from 'react';

function FormWithValidation() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    password: '',
    confirmPassword: ''
  });

  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  // 验证规则
  const validationRules = {
    name: (value) => {
      if (!value.trim()) return '姓名不能为空';
      if (value.length < 2) return '姓名至少2个字符';
      if (value.length > 20) return '姓名不能超过20个字符';
      return '';
    },
    email: (value) => {
      if (!value.trim()) return '邮箱不能为空';
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!emailRegex.test(value)) return '请输入有效的邮箱地址';
      return '';
    },
    password: (value) => {
      if (!value.trim()) return '密码不能为空';
      if (value.length < 6) return '密码至少6个字符';
      if (!/[A-Z]/.test(value)) return '密码必须包含大写字母';
      if (!/[a-z]/.test(value)) return '密码必须包含小写字母';
      if (!/[0-9]/.test(value)) return '密码必须包含数字';
      return '';
    },
    confirmPassword: (value) => {
      if (value !== formData.password) return '两次输入的密码不一致';
      return '';
    }
  };

  // 处理输入变化
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData({
      ...formData,
      [name]: value
    });

    // 如果已经触摸过该字段,立即验证
    if (touched[name]) {
      validateField(name, value);
    }
  };

  // 处理字段失去焦点
  const handleBlur = (e) => {
    const { name, value } = e.target;
    setTouched({
      ...touched,
      [name]: true
    });
    validateField(name, value);
  };

  // 验证单个字段
  const validateField = (name, value) => {
    const error = validationRules[name] ? validationRules[name](value) : '';
    setErrors({
      ...errors,
      [name]: error
    });
    return !error;
  };

  // 验证整个表单
  const validateForm = () => {
    const newErrors = {};
    let isValid = true;

    Object.keys(validationRules).forEach(name => {
      const error = validationRules[name](formData[name]);
      if (error) {
        newErrors[name] = error;
        isValid = false;
      }
    });

    setErrors(newErrors);
    // 标记所有字段为已触摸
    setTouched(Object.keys(formData).reduce((acc, key) => {
      acc[key] = true;
      return acc;
    }, {}));

    return isValid;
  };

  // 处理表单提交
  const handleSubmit = (e) => {
    e.preventDefault();

    if (validateForm()) {
      console.log('表单验证通过:', formData);
      // 提交数据到服务器
      alert('表单提交成功!');
    } else {
      console.log('表单验证失败:', errors);
      alert('请修正表单错误后再提交。');
    }
  };

  // 检查字段是否有效
  const getFieldStatus = (name) => {
    if (!touched[name]) return ''; // 未触摸
    return errors[name] ? 'is-invalid' : 'is-valid';
  };

  return (
    <form onSubmit={handleSubmit} className="form-with-validation">
      <h3>注册表单</h3>

      <div className="form-group">
        <label htmlFor="name">姓名 *</label>
        <input
          type="text"
          id="name"
          name="name"
          value={formData.name}
          onChange={handleChange}
          onBlur={handleBlur}
          className={`form-control ${getFieldStatus('name')}`}
          placeholder="请输入姓名"
        />
        {errors.name && touched.name && (
          <div className="invalid-feedback">{errors.name}</div>
        )}
        {!errors.name && touched.name && (
          <div className="valid-feedback">姓名格式正确</div>
        )}
      </div>

      <div className="form-group">
        <label htmlFor="email">邮箱 *</label>
        <input
          type="email"
          id="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
          onBlur={handleBlur}
          className={`form-control ${getFieldStatus('email')}`}
          placeholder="请输入邮箱"
        />
        {errors.email && touched.email && (
          <div className="invalid-feedback">{errors.email}</div>
        )}
        {!errors.email && touched.email && (
          <div className="valid-feedback">邮箱格式正确</div>
        )}
      </div>

      <div className="form-group">
        <label htmlFor="password">密码 *</label>
        <input
          type="password"
          id="password"
          name="password"
          value={formData.password}
          onChange={handleChange}
          onBlur={handleBlur}
          className={`form-control ${getFieldStatus('password')}`}
          placeholder="请输入密码"
        />
        {errors.password && touched.password && (
          <div className="invalid-feedback">{errors.password}</div>
        )}
        {!errors.password && touched.password && (
          <div className="valid-feedback">密码格式正确</div>
        )}
        <small className="form-text text-muted">
          密码需包含大小写字母和数字,至少6个字符
        </small>
      </div>

      <div className="form-group">
        <label htmlFor="confirmPassword">确认密码 *</label>
        <input
          type="password"
          id="confirmPassword"
          name="confirmPassword"
          value={formData.confirmPassword}
          onChange={handleChange}
          onBlur={handleBlur}
          className={`form-control ${getFieldStatus('confirmPassword')}`}
          placeholder="请再次输入密码"
        />
        {errors.confirmPassword && touched.confirmPassword && (
          <div className="invalid-feedback">{errors.confirmPassword}</div>
        )}
        {!errors.confirmPassword && touched.confirmPassword && (
          <div className="valid-feedback">密码一致</div>
        )}
      </div>

      <div className="form-group form-check">
        <input
          type="checkbox"
          id="terms"
          name="terms"
          required
          className="form-check-input"
        />
        <label htmlFor="terms" className="form-check-label">
          我已阅读并同意服务条款
        </label>
        <div className="invalid-feedback">
          必须同意服务条款才能提交
        </div>
      </div>

      <button type="submit" className="btn btn-primary">
        注册
      </button>
      <button
        type="button"
        className="btn btn-secondary"
        onClick={() => {
          setFormData({
            name: '',
            email: '',
            password: '',
            confirmPassword: ''
          });
          setErrors({});
          setTouched({});
        }}
      >
        重置
      </button>

      <div className="form-summary mt-3">
        <p>
          验证状态: {Object.keys(errors).length === 0 ?
            '✓ 所有字段验证通过' :
            `✗ 有 ${Object.keys(errors).length} 个字段需要修正`}
        </p>
      </div>
    </form>
  );
}
                    

动态表单

动态表单允许用户动态添加、删除和编辑表单字段。

动态表单示例


import React, { useState } from 'react';

function DynamicContactForm() {
  const [contacts, setContacts] = useState([
    { id: 1, name: '', email: '', phone: '' }
  ]);

  // 添加联系人
  const addContact = () => {
    const newId = contacts.length > 0 ?
      Math.max(...contacts.map(c => c.id)) + 1 : 1;

    setContacts([
      ...contacts,
      { id: newId, name: '', email: '', phone: '' }
    ]);
  };

  // 删除联系人
  const removeContact = (id) => {
    if (contacts.length <= 1) return; // 至少保留一个
    setContacts(contacts.filter(contact => contact.id !== id));
  };

  // 更新联系人信息
  const updateContact = (id, field, value) => {
    setContacts(contacts.map(contact =>
      contact.id === id ? { ...contact, [field]: value } : contact
    ));
  };

  // 处理表单提交
  const handleSubmit = (e) => {
    e.preventDefault();

    // 验证所有联系人
    const hasErrors = contacts.some(contact =>
      !contact.name.trim() || !contact.email.trim()
    );

    if (hasErrors) {
      alert('请填写所有必填字段');
      return;
    }

    console.log('提交的联系人:', contacts);
    alert(`成功提交 ${contacts.length} 个联系人`);
  };

  return (
    <form onSubmit={handleSubmit} className="dynamic-contact-form">
      <h3>联系人表单</h3>
      <p>您可以添加多个联系人信息</p>

      {contacts.map((contact, index) => (
        <div key={contact.id} className="contact-card">
          <div className="card-header">
            <h5>联系人 #{index + 1}</h5>
            {contacts.length > 1 && (
              <button
                type="button"
                onClick={() => removeContact(contact.id)}
                className="btn-remove"
              >
                删除
              </button>
            )}
          </div>

          <div className="card-body">
            <div className="form-group">
              <label>姓名 *</label>
              <input
                type="text"
                value={contact.name}
                onChange={(e) => updateContact(contact.id, 'name', e.target.value)}
                placeholder="请输入姓名"
                required
              />
              {!contact.name.trim() && (
                <small className="error-text">姓名不能为空</small>
              )}
            </div>

            <div className="form-group">
              <label>邮箱 *</label>
              <input
                type="email"
                value={contact.email}
                onChange={(e) => updateContact(contact.id, 'email', e.target.value)}
                placeholder="请输入邮箱"
                required
              />
              {!contact.email.trim() && (
                <small className="error-text">邮箱不能为空</small>
              )}
            </div>

            <div className="form-group">
              <label>电话</label>
              <input
                type="tel"
                value={contact.phone}
                onChange={(e) => updateContact(contact.id, 'phone', e.target.value)}
                placeholder="请输入电话"
              />
            </div>
          </div>
        </div>
      ))}

      <div className="form-actions">
        <button
          type="button"
          onClick={addContact}
          className="btn-add"
        >
          + 添加联系人
        </button>

        <button type="submit" className="btn-submit">
          提交所有联系人
        </button>

        <button
          type="button"
          onClick={() => setContacts([{ id: 1, name: '', email: '', phone: '' }])}
          className="btn-reset"
        >
          重置表单
        </button>
      </div>

      <div className="form-info">
        <p>当前联系人数量: {contacts.length}</p>
        <p>* 为必填字段</p>
      </div>
    </form>
  );
}
                        

第三方表单库

对于复杂的表单需求,可以使用第三方表单库来简化开发。

Formik

最流行的React表单库,简化表单处理流程。


import { Formik, Form, Field } from 'formik';

<Formik
  initialValues={{ email: '' }}
  onSubmit={(values) => {
    console.log(values);
  }}
>
  <Form>
    <Field type="email" name="email" />
    <button type="submit">提交</button>
  </Form>
</Formik>
                                    
React Hook Form

高性能表单库,使用React Hooks,非受控组件。


import { useForm } from 'react-hook-form';

const { register, handleSubmit } = useForm();

<form onSubmit={handleSubmit(onSubmit)}>
  <input {...register("email")} />
  <button type="submit">提交</button>
</form>
                                    
Final Form

高性能表单库,订阅式API,支持复杂的表单逻辑。


import { Form, Field } from 'react-final-form';

<Form
  onSubmit={onSubmit}
  render={({ handleSubmit }) => (
    <form onSubmit={handleSubmit}>
      <Field name="email" component="input" />
      <button type="submit">提交</button>
    </form>
  )}
/>
                                    

文件上传处理


import React, { useState, useRef } from 'react';

function FileUploadForm() {
  const [file, setFile] = useState(null);
  const [preview, setPreview] = useState(null);
  const [uploadProgress, setUploadProgress] = useState(0);
  const fileInputRef = useRef(null);

  // 处理文件选择
  const handleFileChange = (e) => {
    const selectedFile = e.target.files[0];

    if (selectedFile) {
      // 验证文件类型
      const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
      if (!allowedTypes.includes(selectedFile.type)) {
        alert('只支持JPG、PNG和GIF格式的图片');
        return;
      }

      // 验证文件大小(最大5MB)
      if (selectedFile.size > 5 * 1024 * 1024) {
        alert('文件大小不能超过5MB');
        return;
      }

      setFile(selectedFile);

      // 创建预览
      const reader = new FileReader();
      reader.onloadend = () => {
        setPreview(reader.result);
      };
      reader.readAsDataURL(selectedFile);
    }
  };

  // 模拟文件上传
  const handleUpload = async () => {
    if (!file) {
      alert('请先选择文件');
      return;
    }

    // 创建FormData对象
    const formData = new FormData();
    formData.append('file', file);
    formData.append('uploadedAt', new Date().toISOString());

    try {
      // 模拟上传进度
      const interval = setInterval(() => {
        setUploadProgress(prev => {
          const newProgress = prev + 10;
          if (newProgress >= 100) {
            clearInterval(interval);
            return 100;
          }
          return newProgress;
        });
      }, 200);

      // 这里应该是实际的API调用
      // const response = await fetch('/api/upload', {
      //   method: 'POST',
      //   body: formData
      // });

      setTimeout(() => {
        clearInterval(interval);
        setUploadProgress(100);
        alert('文件上传成功!');
        resetForm();
      }, 2000);

    } catch (error) {
      console.error('上传失败:', error);
      alert('文件上传失败');
    }
  };

  // 重置表单
  const resetForm = () => {
    setFile(null);
    setPreview(null);
    setUploadProgress(0);
    if (fileInputRef.current) {
      fileInputRef.current.value = '';
    }
  };

  // 拖拽上传处理
  const handleDragOver = (e) => {
    e.preventDefault();
    e.stopPropagation();
  };

  const handleDrop = (e) => {
    e.preventDefault();
    e.stopPropagation();

    if (e.dataTransfer.files && e.dataTransfer.files[0]) {
      const droppedFile = e.dataTransfer.files[0];

      // 模拟文件选择事件
      const event = {
        target: {
          files: [droppedFile]
        }
      };
      handleFileChange(event);
    }
  };

  return (
    <div className="file-upload-form">
      <h3>文件上传</h3>

      <div
        className={`drop-zone ${preview ? 'has-file' : ''}`}
        onDragOver={handleDragOver}
        onDrop={handleDrop}
        onClick={() => fileInputRef.current?.click()}
      >
        {preview ? (
          <div className="file-preview">
            <img src={preview} alt="预览" />
            <p>已选择: {file.name}</p>
            <p>大小: {(file.size / 1024 / 1024).toFixed(2)} MB</p>
          </div>
        ) : (
          <div className="drop-zone-content">
            <i className="fas fa-cloud-upload-alt fa-3x"></i>
            <p>点击或拖拽文件到这里</p>
            <p className="upload-hint">支持JPG、PNG、GIF,最大5MB</p>
          </div>
        )}
      </div>

      <input
        type="file"
        ref={fileInputRef}
        onChange={handleFileChange}
        accept="image/jpeg,image/png,image/gif"
        style={display: 'none'}
      />

      {file && (
        <div className="upload-progress">
          <div className="progress-bar">
            <div
              className="progress-fill"
              style={width: `${uploadProgress}%`}
            ></div>
          </div>
          <span>{uploadProgress}%</span>
        </div>
      )}

      <div className="upload-actions">
        <button
          type="button"
          onClick={handleUpload}
          disabled={!file || uploadProgress > 0}
          className="btn-upload"
        >
          上传文件
        </button>
        <button
          type="button"
          onClick={resetForm}
          className="btn-reset"
        >
          重置
        </button>
      </div>
    </div>
  );
}
                    

表单处理最佳实践

推荐做法
  • 使用受控组件进行实时验证和即时反馈
  • 为每个表单字段提供清晰的标签和提示
  • 在用户输入时提供实时验证反馈
  • 使用htmlForid关联标签和输入框
  • 为表单提交提供加载状态和禁用状态
  • 对复杂表单使用表单库(如Formik、React Hook Form)
  • 确保表单在所有设备上都能正常使用
避免做法
  • 避免在表单提交时才显示所有错误
  • 不要忘记处理表单重置和取消操作
  • 避免使用内联样式控制表单布局
  • 不要在每次输入时都重新渲染整个表单
  • 避免过度复杂的表单验证逻辑
  • 不要混合使用受控和非受控组件
  • 避免在移动设备上使用不友好的输入类型

完整示例:用户注册表单


import React, { useState } from 'react';

function UserRegistrationForm() {
  const [formData, setFormData] = useState({
    personalInfo: {
      firstName: '',
      lastName: '',
      birthDate: '',
      gender: ''
    },
    contactInfo: {
      email: '',
      phone: '',
      address: '',
      city: '',
      zipCode: ''
    },
    accountInfo: {
      username: '',
      password: '',
      confirmPassword: '',
      newsletter: true,
      termsAccepted: false
    }
  });

  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [activeSection, setActiveSection] = useState('personal');

  // 表单验证规则
  const validate = () => {
    const newErrors = {};

    // 验证个人信息
    if (!formData.personalInfo.firstName.trim()) {
      newErrors.firstName = '名字不能为空';
    }

    if (!formData.personalInfo.lastName.trim()) {
      newErrors.lastName = '姓氏不能为空';
    }

    // 验证联系信息
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!formData.contactInfo.email.trim()) {
      newErrors.email = '邮箱不能为空';
    } else if (!emailRegex.test(formData.contactInfo.email)) {
      newErrors.email = '请输入有效的邮箱地址';
    }

    // 验证账户信息
    if (!formData.accountInfo.username.trim()) {
      newErrors.username = '用户名不能为空';
    } else if (formData.accountInfo.username.length < 3) {
      newErrors.username = '用户名至少3个字符';
    }

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

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

    if (!formData.accountInfo.termsAccepted) {
      newErrors.termsAccepted = '必须接受服务条款';
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  // 处理输入变化
  const handleChange = (section, field, value) => {
    setFormData({
      ...formData,
      [section]: {
        ...formData[section],
        [field]: value
      }
    });

    // 清除该字段的错误
    if (errors[field]) {
      setErrors({
        ...errors,
        [field]: ''
      });
    }
  };

  // 处理表单提交
  const handleSubmit = async (e) => {
    e.preventDefault();

    if (!validate()) {
      alert('请修正表单错误后再提交');
      return;
    }

    setIsSubmitting(true);

    try {
      // 模拟API调用
      await new Promise(resolve => setTimeout(resolve, 2000));

      console.log('表单数据:', formData);
      alert('注册成功!');

      // 重置表单
      setFormData({
        personalInfo: {
          firstName: '',
          lastName: '',
          birthDate: '',
          gender: ''
        },
        contactInfo: {
          email: '',
          phone: '',
          address: '',
          city: '',
          zipCode: ''
        },
        accountInfo: {
          username: '',
          password: '',
          confirmPassword: '',
          newsletter: true,
          termsAccepted: false
        }
      });
      setErrors({});
      setActiveSection('personal');

    } catch (error) {
      console.error('注册失败:', error);
      alert('注册失败,请稍后重试');
    } finally {
      setIsSubmitting(false);
    }
  };

  // 表单部分组件
  const PersonalInfoSection = () => (
    <div className="form-section">
      <h4>个人信息</h4>
      <div className="form-row">
        <div className="form-group">
          <label>名字 *</label>
          <input
            type="text"
            value={formData.personalInfo.firstName}
            onChange={(e) => handleChange('personalInfo', 'firstName', e.target.value)}
            className={errors.firstName ? 'error' : ''}
          />
          {errors.firstName && <span className="error-message">{errors.firstName}</span>}
        </div>
        <div className="form-group">
          <label>姓氏 *</label>
          <input
            type="text"
            value={formData.personalInfo.lastName}
            onChange={(e) => handleChange('personalInfo', 'lastName', e.target.value)}
            className={errors.lastName ? 'error' : ''}
          />
          {errors.lastName && <span className="error-message">{errors.lastName}</span>}
        </div>
      </div>

      <div className="form-row">
        <div className="form-group">
          <label>出生日期</label>
          <input
            type="date"
            value={formData.personalInfo.birthDate}
            onChange={(e) => handleChange('personalInfo', 'birthDate', e.target.value)}
          />
        </div>
        <div className="form-group">
          <label>性别</label>
          <select
            value={formData.personalInfo.gender}
            onChange={(e) => handleChange('personalInfo', 'gender', e.target.value)}
          >
            <option value="">请选择</option>
            <option value="male">男</option>
            <option value="female">女</option>
            <option value="other">其他</option>
          </select>
        </div>
      </div>
    </div>
  );

  const ContactInfoSection = () => (
    <div className="form-section">
      <h4>联系信息</h4>
      <div className="form-group">
        <label>邮箱 *</label>
        <input
          type="email"
          value={formData.contactInfo.email}
          onChange={(e) => handleChange('contactInfo', 'email', e.target.value)}
          className={errors.email ? 'error' : ''}
        />
        {errors.email && <span className="error-message">{errors.email}</span>}
      </div>

      <div className="form-group">
        <label>电话</label>
        <input
          type="tel"
          value={formData.contactInfo.phone}
          onChange={(e) => handleChange('contactInfo', 'phone', e.target.value)}
        />
      </div>

      <div className="form-group">
        <label>地址</label>
        <textarea
          value={formData.contactInfo.address}
          onChange={(e) => handleChange('contactInfo', 'address', e.target.value)}
          rows="3"
        />
      </div>

      <div className="form-row">
        <div className="form-group">
          <label>城市</label>
          <input
            type="text"
            value={formData.contactInfo.city}
            onChange={(e) => handleChange('contactInfo', 'city', e.target.value)}
          />
        </div>
        <div className="form-group">
          <label>邮政编码</label>
          <input
            type="text"
            value={formData.contactInfo.zipCode}
            onChange={(e) => handleChange('contactInfo', 'zipCode', e.target.value)}
          />
        </div>
      </div>
    </div>
  );

  const AccountInfoSection = () => (
    <div className="form-section">
      <h4>账户信息</h4>
      <div className="form-group">
        <label>用户名 *</label>
        <input
          type="text"
          value={formData.accountInfo.username}
          onChange={(e) => handleChange('accountInfo', 'username', e.target.value)}
          className={errors.username ? 'error' : ''}
        />
        {errors.username && <span className="error-message">{errors.username}</span>}
      </div>

      <div className="form-row">
        <div className="form-group">
          <label>密码 *</label>
          <input
            type="password"
            value={formData.accountInfo.password}
            onChange={(e) => handleChange('accountInfo', 'password', e.target.value)}
            className={errors.password ? 'error' : ''}
          />
          {errors.password && <span className="error-message">{errors.password}</span>}
        </div>
        <div className="form-group">
          <label>确认密码 *</label>
          <input
            type="password"
            value={formData.accountInfo.confirmPassword}
            onChange={(e) => handleChange('accountInfo', 'confirmPassword', e.target.value)}
            className={errors.confirmPassword ? 'error' : ''}
          />
          {errors.confirmPassword && <span className="error-message">{errors.confirmPassword}</span>}
        </div>
      </div>

      <div className="form-check-group">
        <label className="checkbox-label">
          <input
            type="checkbox"
            checked={formData.accountInfo.newsletter}
            onChange={(e) => handleChange('accountInfo', 'newsletter', e.target.checked)}
          />
          订阅新闻和更新
        </label>

        <label className={`checkbox-label ${errors.termsAccepted ? 'error' : ''}`}>
          <input
            type="checkbox"
            checked={formData.accountInfo.termsAccepted}
            onChange={(e) => handleChange('accountInfo', 'termsAccepted', e.target.checked)}
          />
          我同意服务条款和隐私政策 *
        </label>
        {errors.termsAccepted && (
          <span className="error-message">{errors.termsAccepted}</span>
        )}
      </div>
    </div>
  );

  return (
    <form onSubmit={handleSubmit} className="user-registration-form">
      <h2>用户注册</h2>

      <div className="form-navigation">
        <button
          type="button"
          className={`nav-btn ${activeSection === 'personal' ? 'active' : ''}`}
          onClick={() => setActiveSection('personal')}
        >
          个人信息
        </button>
        <button
          type="button"
          className={`nav-btn ${activeSection === 'contact' ? 'active' : ''}`}
          onClick={() => setActiveSection('contact')}
        >
          联系信息
        </button>
        <button
          type="button"
          className={`nav-btn ${activeSection === 'account' ? 'active' : ''}`}
          onClick={() => setActiveSection('account')}
        >
          账户信息
        </button>
      </div>

      {activeSection === 'personal' && <PersonalInfoSection />}
      {activeSection === 'contact' && <ContactInfoSection />}
      {activeSection === 'account' && <AccountInfoSection />}

      <div className="form-actions">
        <button
          type="button"
          onClick={() => {
            if (activeSection === 'contact') setActiveSection('personal');
            if (activeSection === 'account') setActiveSection('contact');
          }}
          disabled={activeSection === 'personal'}
          className="btn-prev"
        >
          上一步
        </button>

        <button
          type="button"
          onClick={() => {
            if (activeSection === 'personal') setActiveSection('contact');
            if (activeSection === 'contact') setActiveSection('account');
          }}
          disabled={activeSection === 'account'}
          className="btn-next"
        >
          下一步
        </button>

        <button
          type="submit"
          disabled={isSubmitting}
          className="btn-submit"
        >
          {isSubmitting ? (
            <>
              <i className="fas fa-spinner fa-spin"></i> 提交中...
            </>
          ) : (
            '完成注册'
          )}
        </button>
      </div>

      <div className="form-footer">
        <p>* 为必填字段</p>
        <p>总共有 {Object.keys(errors).length} 个错误需要修正</p>
      </div>
    </form>
  );
}
                    

本章要点总结

  • 受控组件:表单数据由React state管理,推荐使用
  • 非受控组件:表单数据由DOM管理,通过ref获取值
  • 表单验证:实时验证提供更好的用户体验
  • 动态表单:允许用户动态添加/删除表单字段
  • 文件上传:使用非受控组件处理,支持拖拽上传
  • 第三方库:Formik、React Hook Form等简化复杂表单处理
  • 最佳实践:清晰的标签、实时反馈、无障碍访问