Skip to content
刘恩义的技术博客
返回

React 表单处理最佳实践

Edit page

表单是 Web 应用中最常见的交互元素之一,也是用户与应用进行数据交换的重要方式。在 React 中,表单处理有多种实现方式,从简单的受控组件到复杂的表单库。本文将详细介绍 React 表单处理的最佳实践,包括受控组件、表单验证、性能优化、第三方库使用等,并提供完整的实战案例。

1. 表单处理基础

1.1 受控组件 vs 非受控组件

受控组件

受控组件是 React 中处理表单的推荐方式,表单元素的值由 React 状态控制:

import { useState } from 'react';

function ControlledInput() {
  const [value, setValue] = useState('');
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  };
  
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    console.log('Submitted value:', value);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text" 
        value={value} 
        onChange={handleChange} 
        placeholder="Enter something"
      />
      <button type="submit">Submit</button>
    </form>
  );
}

非受控组件

非受控组件使用 ref 直接访问 DOM 元素的值:

import { useRef } from 'react';

function UncontrolledInput() {
  const inputRef = useRef<HTMLInputElement>(null);
  
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (inputRef.current) {
      console.log('Submitted value:', inputRef.current.value);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text" 
        ref={inputRef} 
        placeholder="Enter something"
        defaultValue="Initial value"
      />
      <button type="submit">Submit</button>
    </form>
  );
}

1.2 选择建议

组件类型适用场景优点缺点
受控组件需要实时验证、格式化、或与其他表单字段联动状态可控,易于验证,支持实时反馈可能导致频繁重渲染
非受控组件简单表单,文件上传,或需要直接访问 DOM实现简单,性能更好状态不可控,难以实现复杂验证

2. 表单状态管理

2.1 单个表单字段

对于单个表单字段,使用 useState 管理状态:

import { useState } from 'react';

function SingleFieldForm() {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');
  
  const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    setEmail(value);
    
    // 简单验证
    if (!value.includes('@')) {
      setError('Please enter a valid email');
    } else {
      setError('');
    }
  };
  
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (!error && email) {
      console.log('Submitted email:', email);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="email">Email:</label>
        <input 
          type="email" 
          id="email"
          value={email} 
          onChange={handleEmailChange} 
          placeholder="Enter your email"
        />
        {error && <p style={{ color: 'red' }}>{error}</p>}
      </div>
      <button type="submit" disabled={!!error || !email}>
        Submit
      </button>
    </form>
  );
}

2.2 多个表单字段

对于多个表单字段,可以使用单个 useState 对象管理所有字段:

import { useState } from 'react';

interface FormData {
  name: string;
  email: string;
  password: string;
}

function MultiFieldForm() {
  const [formData, setFormData] = useState<FormData>({
    name: '',
    email: '',
    password: ''
  });
  const [errors, setErrors] = useState<Partial<FormData>>({
    name: '',
    email: '',
    password: ''
  });
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
    
    // 简单验证
    if (name === 'name' && !value) {
      setErrors(prev => ({ ...prev, name: 'Name is required' }));
    } else if (name === 'email' && !value.includes('@')) {
      setErrors(prev => ({ ...prev, email: 'Please enter a valid email' }));
    } else if (name === 'password' && value.length < 6) {
      setErrors(prev => ({ ...prev, password: 'Password must be at least 6 characters' }));
    } else {
      setErrors(prev => ({ ...prev, [name]: '' }));
    }
  };
  
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    
    // 最终验证
    const newErrors: Partial<FormData> = {};
    if (!formData.name) newErrors.name = 'Name is required';
    if (!formData.email.includes('@')) newErrors.email = 'Please enter a valid email';
    if (formData.password.length < 6) newErrors.password = 'Password must be at least 6 characters';
    
    if (Object.keys(newErrors).length === 0) {
      console.log('Submitted form data:', formData);
    } else {
      setErrors(newErrors);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Name:</label>
        <input 
          type="text" 
          id="name"
          name="name"
          value={formData.name} 
          onChange={handleChange} 
          placeholder="Enter your name"
        />
        {errors.name && <p style={{ color: 'red' }}>{errors.name}</p>}
      </div>
      <div>
        <label htmlFor="email">Email:</label>
        <input 
          type="email" 
          id="email"
          name="email"
          value={formData.email} 
          onChange={handleChange} 
          placeholder="Enter your email"
        />
        {errors.email && <p style={{ color: 'red' }}>{errors.email}</p>}
      </div>
      <div>
        <label htmlFor="password">Password:</label>
        <input 
          type="password" 
          id="password"
          name="password"
          value={formData.password} 
          onChange={handleChange} 
          placeholder="Enter your password"
        />
        {errors.password && <p style={{ color: 'red' }}>{errors.password}</p>}
      </div>
      <button type="submit">Submit</button>
    </form>
  );
}

2.3 使用 useReducer 管理复杂表单

对于复杂表单,可以使用 useReducer 管理状态:

import { useReducer } from 'react';

interface FormState {
  name: string;
  email: string;
  password: string;
  errors: {
    name: string;
    email: string;
    password: string;
  };
  isSubmitting: boolean;
}

type FormAction =
  | { type: 'UPDATE_FIELD'; field: keyof Omit<FormState, 'errors' | 'isSubmitting'>; value: string }
  | { type: 'SET_ERRORS'; errors: Partial<FormState['errors']> }
  | { type: 'SET_SUBMITTING'; isSubmitting: boolean }
  | { type: 'RESET' };

const initialState: FormState = {
  name: '',
  email: '',
  password: '',
  errors: {
    name: '',
    email: '',
    password: '',
  },
  isSubmitting: false,
};

function formReducer(state: FormState, action: FormAction): FormState {
  switch (action.type) {
    case 'UPDATE_FIELD':
      return {
        ...state,
        [action.field]: action.value,
        errors: {
          ...state.errors,
          [action.field]: '',
        },
      };
    case 'SET_ERRORS':
      return {
        ...state,
        errors: {
          ...state.errors,
          ...action.errors,
        },
      };
    case 'SET_SUBMITTING':
      return {
        ...state,
        isSubmitting: action.isSubmitting,
      };
    case 'RESET':
      return initialState;
    default:
      return state;
  }
}

function ComplexForm() {
  const [state, dispatch] = useReducer(formReducer, initialState);
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    dispatch({ type: 'UPDATE_FIELD', field: name as keyof Omit<FormState, 'errors' | 'isSubmitting'>, value });
  };
  
  const validateForm = (): boolean => {
    const errors: Partial<FormState['errors']> = {};
    if (!state.name) errors.name = 'Name is required';
    if (!state.email.includes('@')) errors.email = 'Please enter a valid email';
    if (state.password.length < 6) errors.password = 'Password must be at least 6 characters';
    
    if (Object.keys(errors).length > 0) {
      dispatch({ type: 'SET_ERRORS', errors });
      return false;
    }
    return true;
  };
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    if (!validateForm()) {
      return;
    }
    
    dispatch({ type: 'SET_SUBMITTING', isSubmitting: true });
    
    // 模拟 API 调用
    try {
      await new Promise(resolve => setTimeout(resolve, 1000));
      console.log('Submitted form data:', state);
      dispatch({ type: 'RESET' });
    } catch (error) {
      console.error('Submission error:', error);
    } finally {
      dispatch({ type: 'SET_SUBMITTING', isSubmitting: false });
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Name:</label>
        <input 
          type="text" 
          id="name"
          name="name"
          value={state.name} 
          onChange={handleChange} 
          placeholder="Enter your name"
        />
        {state.errors.name && <p style={{ color: 'red' }}>{state.errors.name}</p>}
      </div>
      <div>
        <label htmlFor="email">Email:</label>
        <input 
          type="email" 
          id="email"
          name="email"
          value={state.email} 
          onChange={handleChange} 
          placeholder="Enter your email"
        />
        {state.errors.email && <p style={{ color: 'red' }}>{state.errors.email}</p>}
      </div>
      <div>
        <label htmlFor="password">Password:</label>
        <input 
          type="password" 
          id="password"
          name="password"
          value={state.password} 
          onChange={handleChange} 
          placeholder="Enter your password"
        />
        {state.errors.password && <p style={{ color: 'red' }}>{state.errors.password}</p>}
      </div>
      <button type="submit" disabled={state.isSubmitting}>
        {state.isSubmitting ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  );
}

3. 表单验证

3.1 客户端验证

内置表单验证

HTML5 提供了内置的表单验证:

function BuiltInValidationForm() {
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    const form = e.currentTarget;
    if (form.checkValidity()) {
      console.log('Form is valid');
    } else {
      form.reportValidity();
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Name:</label>
        <input 
          type="text" 
          id="name"
          name="name"
          required
          minLength={2}
          placeholder="Enter your name"
        />
      </div>
      <div>
        <label htmlFor="email">Email:</label>
        <input 
          type="email" 
          id="email"
          name="email"
          required
          placeholder="Enter your email"
        />
      </div>
      <div>
        <label htmlFor="password">Password:</label>
        <input 
          type="password" 
          id="password"
          name="password"
          required
          minLength={6}
          placeholder="Enter your password"
        />
      </div>
      <button type="submit">Submit</button>
    </form>
  );
}

自定义验证

使用 JavaScript 实现自定义验证逻辑:

import { useState } from 'react';

interface FormData {
  name: string;
  email: string;
  password: string;
  confirmPassword: string;
}

function CustomValidationForm() {
  const [formData, setFormData] = useState<FormData>({
    name: '',
    email: '',
    password: '',
    confirmPassword: ''
  });
  const [errors, setErrors] = useState<Partial<FormData>>({});
  
  const validateField = (name: keyof FormData, value: string) => {
    let error = '';
    
    switch (name) {
      case 'name':
        if (!value) {
          error = 'Name is required';
        } else if (value.length < 2) {
          error = 'Name must be at least 2 characters';
        }
        break;
      case 'email':
        if (!value) {
          error = 'Email is required';
        } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
          error = 'Please enter a valid email';
        }
        break;
      case 'password':
        if (!value) {
          error = 'Password is required';
        } else if (value.length < 6) {
          error = 'Password must be at least 6 characters';
        } else if (!/[A-Z]/.test(value)) {
          error = 'Password must contain at least one uppercase letter';
        } else if (!/[0-9]/.test(value)) {
          error = 'Password must contain at least one number';
        }
        break;
      case 'confirmPassword':
        if (!value) {
          error = 'Please confirm your password';
        } else if (value !== formData.password) {
          error = 'Passwords do not match';
        }
        break;
      default:
        break;
    }
    
    return error;
  };
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
    
    const error = validateField(name as keyof FormData, value);
    setErrors(prev => ({
      ...prev,
      [name]: error
    }));
  };
  
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    
    // 验证所有字段
    const newErrors: Partial<FormData> = {};
    Object.entries(formData).forEach(([name, value]) => {
      const error = validateField(name as keyof FormData, value);
      if (error) {
        newErrors[name as keyof FormData] = error;
      }
    });
    
    if (Object.keys(newErrors).length === 0) {
      console.log('Submitted form data:', formData);
    } else {
      setErrors(newErrors);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Name:</label>
        <input 
          type="text" 
          id="name"
          name="name"
          value={formData.name} 
          onChange={handleChange} 
          placeholder="Enter your name"
        />
        {errors.name && <p style={{ color: 'red' }}>{errors.name}</p>}
      </div>
      <div>
        <label htmlFor="email">Email:</label>
        <input 
          type="email" 
          id="email"
          name="email"
          value={formData.email} 
          onChange={handleChange} 
          placeholder="Enter your email"
        />
        {errors.email && <p style={{ color: 'red' }}>{errors.email}</p>}
      </div>
      <div>
        <label htmlFor="password">Password:</label>
        <input 
          type="password" 
          id="password"
          name="password"
          value={formData.password} 
          onChange={handleChange} 
          placeholder="Enter your password"
        />
        {errors.password && <p style={{ color: 'red' }}>{errors.password}</p>}
      </div>
      <div>
        <label htmlFor="confirmPassword">Confirm Password:</label>
        <input 
          type="password" 
          id="confirmPassword"
          name="confirmPassword"
          value={formData.confirmPassword} 
          onChange={handleChange} 
          placeholder="Confirm your password"
        />
        {errors.confirmPassword && <p style={{ color: 'red' }}>{errors.confirmPassword}</p>}
      </div>
      <button type="submit">Submit</button>
    </form>
  );
}

3.2 服务端验证

服务端验证是确保数据安全性的重要环节:

import { useState } from 'react';

interface FormData {
  email: string;
  password: string;
}

function ServerValidationForm() {
  const [formData, setFormData] = useState<FormData>({
    email: '',
    password: ''
  });
  const [errors, setErrors] = useState<Partial<FormData>>({});
  const [serverError, setServerError] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
    setErrors(prev => ({
      ...prev,
      [name]: ''
    }));
    setServerError('');
  };
  
  const validateForm = (): boolean => {
    const newErrors: Partial<FormData> = {};
    if (!formData.email) newErrors.email = 'Email is required';
    if (!formData.email.includes('@')) newErrors.email = 'Please enter a valid email';
    if (!formData.password) newErrors.password = 'Password is required';
    
    if (Object.keys(newErrors).length > 0) {
      setErrors(newErrors);
      return false;
    }
    return true;
  };
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    if (!validateForm()) {
      return;
    }
    
    setIsSubmitting(true);
    setServerError('');
    
    try {
      // 模拟 API 调用
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(formData)
      });
      
      if (!response.ok) {
        const errorData = await response.json();
        if (errorData.errors) {
          setErrors(errorData.errors);
        } else {
          setServerError('Invalid email or password');
        }
        return;
      }
      
      const data = await response.json();
      console.log('Login successful:', data);
    } catch (error) {
      setServerError('An error occurred. Please try again.');
    } finally {
      setIsSubmitting(false);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      {serverError && <p style={{ color: 'red' }}>{serverError}</p>}
      <div>
        <label htmlFor="email">Email:</label>
        <input 
          type="email" 
          id="email"
          name="email"
          value={formData.email} 
          onChange={handleChange} 
          placeholder="Enter your email"
        />
        {errors.email && <p style={{ color: 'red' }}>{errors.email}</p>}
      </div>
      <div>
        <label htmlFor="password">Password:</label>
        <input 
          type="password" 
          id="password"
          name="password"
          value={formData.password} 
          onChange={handleChange} 
          placeholder="Enter your password"
        />
        {errors.password && <p style={{ color: 'red' }}>{errors.password}</p>}
      </div>
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Logging in...' : 'Login'}
      </button>
    </form>
  );
}

4. 表单性能优化

4.1 防抖与节流

使用防抖减少频繁的状态更新:

import { useState, useCallback } from 'react';

function DebouncedInput() {
  const [value, setValue] = useState('');
  const [debouncedValue, setDebouncedValue] = useState('');
  
  // 防抖函数
  const useDebounce = (callback: Function, delay: number) => {
    const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | null>(null);
    
    return useCallback((...args: any[]) => {
      if (timeoutId) {
        clearTimeout(timeoutId);
      }
      const id = setTimeout(() => {
        callback(...args);
      }, delay);
      setTimeoutId(id);
    }, [callback, delay, timeoutId]);
  };
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
    debouncedHandleChange(e.target.value);
  };
  
  const debouncedHandleChange = useDebounce((newValue: string) => {
    setDebouncedValue(newValue);
    // 这里可以进行 API 调用等昂贵操作
    console.log('Debounced value:', newValue);
  }, 500);
  
  return (
    <div>
      <input 
        type="text" 
        value={value} 
        onChange={handleChange} 
        placeholder="Type something..."
      />
      <p>Current value: {value}</p>
      <p>Debounced value: {debouncedValue}</p>
    </div>
  );
}

4.2 批量更新

使用 useReducer 或批量更新函数减少重渲染:

import { useReducer } from 'react';

interface FormState {
  firstName: string;
  lastName: string;
  email: string;
  phone: string;
  address: string;
  city: string;
  state: string;
  zip: string;
}

type FormAction =
  | { type: 'UPDATE_FIELD'; field: keyof FormState; value: string }
  | { type: 'RESET' };

const initialState: FormState = {
  firstName: '',
  lastName: '',
  email: '',
  phone: '',
  address: '',
  city: '',
  state: '',
  zip: '',
};

function formReducer(state: FormState, action: FormAction): FormState {
  switch (action.type) {
    case 'UPDATE_FIELD':
      return {
        ...state,
        [action.field]: action.value,
      };
    case 'RESET':
      return initialState;
    default:
      return state;
  }
}

function PerformanceOptimizedForm() {
  const [state, dispatch] = useReducer(formReducer, initialState);
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    dispatch({ type: 'UPDATE_FIELD', field: name as keyof FormState, value });
  };
  
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    console.log('Submitted form data:', state);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
        <div>
          <label htmlFor="firstName">First Name:</label>
          <input 
            type="text" 
            id="firstName"
            name="firstName"
            value={state.firstName} 
            onChange={handleChange} 
            placeholder="Enter your first name"
          />
        </div>
        <div>
          <label htmlFor="lastName">Last Name:</label>
          <input 
            type="text" 
            id="lastName"
            name="lastName"
            value={state.lastName} 
            onChange={handleChange} 
            placeholder="Enter your last name"
          />
        </div>
        <div>
          <label htmlFor="email">Email:</label>
          <input 
            type="email" 
            id="email"
            name="email"
            value={state.email} 
            onChange={handleChange} 
            placeholder="Enter your email"
          />
        </div>
        <div>
          <label htmlFor="phone">Phone:</label>
          <input 
            type="tel" 
            id="phone"
            name="phone"
            value={state.phone} 
            onChange={handleChange} 
            placeholder="Enter your phone number"
          />
        </div>
        <div className="md:col-span-2">
          <label htmlFor="address">Address:</label>
          <input 
            type="text" 
            id="address"
            name="address"
            value={state.address} 
            onChange={handleChange} 
            placeholder="Enter your address"
          />
        </div>
        <div>
          <label htmlFor="city">City:</label>
          <input 
            type="text" 
            id="city"
            name="city"
            value={state.city} 
            onChange={handleChange} 
            placeholder="Enter your city"
          />
        </div>
        <div>
          <label htmlFor="state">State:</label>
          <input 
            type="text" 
            id="state"
            name="state"
            value={state.state} 
            onChange={handleChange} 
            placeholder="Enter your state"
          />
        </div>
        <div>
          <label htmlFor="zip">Zip Code:</label>
          <input 
            type="text" 
            id="zip"
            name="zip"
            value={state.zip} 
            onChange={handleChange} 
            placeholder="Enter your zip code"
          />
        </div>
      </div>
      <button type="submit">Submit</button>
    </form>
  );
}

4.3 虚拟化长表单

对于长表单,可以使用虚拟化技术只渲染可视区域的表单字段:

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

interface FormField {
  id: string;
  name: string;
  label: string;
  type: string;
  placeholder: string;
}

const formFields: FormField[] = [
  { id: '1', name: 'field1', label: 'Field 1', type: 'text', placeholder: 'Enter field 1' },
  { id: '2', name: 'field2', label: 'Field 2', type: 'text', placeholder: 'Enter field 2' },
  { id: '3', name: 'field3', label: 'Field 3', type: 'text', placeholder: 'Enter field 3' },
  // ... 更多字段
  { id: '50', name: 'field50', label: 'Field 50', type: 'text', placeholder: 'Enter field 50' },
];

function VirtualizedForm() {
  const [formData, setFormData] = useState<Record<string, string>>({});
  const containerRef = useRef<HTMLDivElement>(null);
  const [visibleFields, setVisibleFields] = useState<FormField[]>([]);
  
  // 简单的虚拟化实现
  useMemo(() => {
    // 这里可以根据容器的滚动位置和高度计算可见字段
    // 简化实现:只显示前 10 个字段
    setVisibleFields(formFields.slice(0, 10));
  }, []);
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };
  
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    console.log('Submitted form data:', formData);
  };
  
  return (
    <div ref={containerRef} style={{ maxHeight: '400px', overflow: 'auto' }}>
      <form onSubmit={handleSubmit}>
        {visibleFields.map(field => (
          <div key={field.id} style={{ marginBottom: '16px' }}>
            <label htmlFor={field.name}>{field.label}:</label>
            <input 
              type={field.type} 
              id={field.name}
              name={field.name}
              value={formData[field.name] || ''} 
              onChange={handleChange} 
              placeholder={field.placeholder}
            />
          </div>
        ))}
        <button type="submit">Submit</button>
      </form>
    </div>
  );
}

5. 第三方表单库

5.1 Formik

Formik 是一个流行的表单库,简化了表单状态管理和验证:

import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';

interface FormValues {
  name: string;
  email: string;
  password: string;
}

const validationSchema = Yup.object({
  name: Yup.string()
    .required('Name is required')
    .min(2, 'Name must be at least 2 characters'),
  email: Yup.string()
    .required('Email is required')
    .email('Please enter a valid email'),
  password: Yup.string()
    .required('Password is required')
    .min(6, 'Password must be at least 6 characters')
    .matches(/[A-Z]/, 'Password must contain at least one uppercase letter')
    .matches(/[0-9]/, 'Password must contain at least one number'),
});

function FormikForm() {
  const initialValues: FormValues = {
    name: '',
    email: '',
    password: '',
  };
  
  const handleSubmit = (values: FormValues) => {
    console.log('Submitted form data:', values);
  };
  
  return (
    <Formik
      initialValues={initialValues}
      validationSchema={validationSchema}
      onSubmit={handleSubmit}
    >
      <Form>
        <div>
          <label htmlFor="name">Name:</label>
          <Field 
            type="text" 
            id="name"
            name="name"
            placeholder="Enter your name"
          />
          <ErrorMessage name="name" component="p" style={{ color: 'red' }} />
        </div>
        <div>
          <label htmlFor="email">Email:</label>
          <Field 
            type="email" 
            id="email"
            name="email"
            placeholder="Enter your email"
          />
          <ErrorMessage name="email" component="p" style={{ color: 'red' }} />
        </div>
        <div>
          <label htmlFor="password">Password:</label>
          <Field 
            type="password" 
            id="password"
            name="password"
            placeholder="Enter your password"
          />
          <ErrorMessage name="password" component="p" style={{ color: 'red' }} />
        </div>
        <button type="submit">Submit</button>
      </Form>
    </Formik>
  );
}

5.2 React Hook Form

React Hook Form 是一个轻量级的表单库,注重性能:

import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';

interface FormValues {
  name: string;
  email: string;
  password: string;
}

const validationSchema = Yup.object({
  name: Yup.string()
    .required('Name is required')
    .min(2, 'Name must be at least 2 characters'),
  email: Yup.string()
    .required('Email is required')
    .email('Please enter a valid email'),
  password: Yup.string()
    .required('Password is required')
    .min(6, 'Password must be at least 6 characters'),
});

function ReactHookForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>({
    resolver: yupResolver(validationSchema),
    defaultValues: {
      name: '',
      email: '',
      password: '',
    },
  });
  
  const onSubmit = (data: FormValues) => {
    console.log('Submitted form data:', data);
  };
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="name">Name:</label>
        <input 
          type="text" 
          id="name"
          {...register('name')} 
          placeholder="Enter your name"
        />
        {errors.name && <p style={{ color: 'red' }}>{errors.name.message}</p>}
      </div>
      <div>
        <label htmlFor="email">Email:</label>
        <input 
          type="email" 
          id="email"
          {...register('email')} 
          placeholder="Enter your email"
        />
        {errors.email && <p style={{ color: 'red' }}>{errors.email.message}</p>}
      </div>
      <div>
        <label htmlFor="password">Password:</label>
        <input 
          type="password" 
          id="password"
          {...register('password')} 
          placeholder="Enter your password"
        />
        {errors.password && <p style={{ color: 'red' }}>{errors.password.message}</p>}
      </div>
      <button type="submit">Submit</button>
    </form>
  );
}

5.3 Final Form

Final Form 是一个框架无关的表单库,提供了强大的表单状态管理:

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

interface FormValues {
  name: string;
  email: string;
  password: string;
}

function FinalFormForm() {
  const validate = (values: FormValues) => {
    const errors: Partial<FormValues> = {};
    if (!values.name) {
      errors.name = 'Name is required';
    } else if (values.name.length < 2) {
      errors.name = 'Name must be at least 2 characters';
    }
    if (!values.email) {
      errors.email = 'Email is required';
    } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) {
      errors.email = 'Please enter a valid email';
    }
    if (!values.password) {
      errors.password = 'Password is required';
    } else if (values.password.length < 6) {
      errors.password = 'Password must be at least 6 characters';
    }
    return errors;
  };
  
  const onSubmit = (values: FormValues) => {
    console.log('Submitted form data:', values);
  };
  
  return (
    <Form
      onSubmit={onSubmit}
      validate={validate}
      render={({ handleSubmit, submitting, pristine }) => (
        <form onSubmit={handleSubmit}>
          <div>
            <label htmlFor="name">Name:</label>
            <Field name="name">
              {({ input, meta }) => (
                <>
                  <input 
                    {...input} 
                    type="text" 
                    id="name"
                    placeholder="Enter your name"
                  />
                  {meta.error && meta.touched && <p style={{ color: 'red' }}>{meta.error}</p>}
                </>
              )}
            </Field>
          </div>
          <div>
            <label htmlFor="email">Email:</label>
            <Field name="email">
              {({ input, meta }) => (
                <>
                  <input 
                    {...input} 
                    type="email" 
                    id="email"
                    placeholder="Enter your email"
                  />
                  {meta.error && meta.touched && <p style={{ color: 'red' }}>{meta.error}</p>}
                </>
              )}
            </Field>
          </div>
          <div>
            <label htmlFor="password">Password:</label>
            <Field name="password">
              {({ input, meta }) => (
                <>
                  <input 
                    {...input} 
                    type="password" 
                    id="password"
                    placeholder="Enter your password"
                  />
                  {meta.error && meta.touched && <p style={{ color: 'red' }}>{meta.error}</p>}
                </>
              )}
            </Field>
          </div>
          <button type="submit" disabled={submitting || pristine}>
            {submitting ? 'Submitting...' : 'Submit'}
          </button>
        </form>
      )}
    />
  );
}

6. 实战案例

6.1 登录表单

功能需求

实现

import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

interface LoginFormData {
  email: string;
  password: string;
}

function LoginForm() {
  const [formData, setFormData] = useState<LoginFormData>({
    email: '',
    password: ''
  });
  const [errors, setErrors] = useState<Partial<LoginFormData>>({});
  const [serverError, setServerError] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);
  const navigate = useNavigate();
  
  const validateForm = (): boolean => {
    const newErrors: Partial<LoginFormData> = {};
    if (!formData.email) {
      newErrors.email = 'Email is required';
    } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
      newErrors.email = 'Please enter a valid email';
    }
    if (!formData.password) {
      newErrors.password = 'Password is required';
    }
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
    setErrors(prev => ({
      ...prev,
      [name]: ''
    }));
    setServerError('');
  };
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    if (!validateForm()) {
      return;
    }
    
    setIsSubmitting(true);
    setServerError('');
    
    try {
      // 模拟 API 调用
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(formData)
      });
      
      if (!response.ok) {
        const errorData = await response.json();
        if (errorData.errors) {
          setErrors(errorData.errors);
        } else {
          setServerError('Invalid email or password');
        }
        return;
      }
      
      const data = await response.json();
      console.log('Login successful:', data);
      navigate('/dashboard');
    } catch (error) {
      setServerError('An error occurred. Please try again.');
    } finally {
      setIsSubmitting(false);
    }
  };
  
  return (
    <div className="login-form">
      <h1>Login</h1>
      {serverError && <p className="server-error">{serverError}</p>}
      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <label htmlFor="email">Email:</label>
          <input 
            type="email" 
            id="email"
            name="email"
            value={formData.email} 
            onChange={handleChange} 
            placeholder="Enter your email"
            disabled={isSubmitting}
          />
          {errors.email && <p className="error-message">{errors.email}</p>}
        </div>
        <div className="form-group">
          <label htmlFor="password">Password:</label>
          <input 
            type="password" 
            id="password"
            name="password"
            value={formData.password} 
            onChange={handleChange} 
            placeholder="Enter your password"
            disabled={isSubmitting}
          />
          {errors.password && <p className="error-message">{errors.password}</p>}
        </div>
        <div className="form-actions">
          <button 
            type="submit" 
            className="login-button"
            disabled={isSubmitting}
          >
            {isSubmitting ? 'Logging in...' : 'Login'}
          </button>
          <a href="/forgot-password" className="forgot-password">
            Forgot password?
          </a>
        </div>
      </form>
      <div className="register-link">
        Don't have an account? <a href="/register">Register</a>
      </div>
    </div>
  );
}

export default LoginForm;

6.2 注册表单

功能需求

实现

import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

interface RegisterFormData {
  firstName: string;
  lastName: string;
  email: string;
  password: string;
  confirmPassword: string;
}

function RegisterForm() {
  const [formData, setFormData] = useState<RegisterFormData>({
    firstName: '',
    lastName: '',
    email: '',
    password: '',
    confirmPassword: ''
  });
  const [errors, setErrors] = useState<Partial<RegisterFormData>>({});
  const [serverError, setServerError] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);
  const navigate = useNavigate();
  
  const getPasswordStrength = (password: string): 'weak' | 'medium' | 'strong' => {
    if (password.length < 6) return 'weak';
    if (password.length < 8) return 'medium';
    if (/[A-Z]/.test(password) && /[0-9]/.test(password)) return 'strong';
    return 'medium';
  };
  
  const validateField = (name: keyof RegisterFormData, value: string): string => {
    switch (name) {
      case 'firstName':
        if (!value) return 'First name is required';
        if (value.length < 2) return 'First name must be at least 2 characters';
        return '';
      case 'lastName':
        if (!value) return 'Last name is required';
        if (value.length < 2) return 'Last name must be at least 2 characters';
        return '';
      case 'email':
        if (!value) return 'Email is required';
        if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Please enter a valid email';
        return '';
      case 'password':
        if (!value) return 'Password is required';
        if (value.length < 6) return 'Password must be at least 6 characters';
        if (!/[A-Z]/.test(value)) return 'Password must contain at least one uppercase letter';
        if (!/[0-9]/.test(value)) return 'Password must contain at least one number';
        return '';
      case 'confirmPassword':
        if (!value) return 'Please confirm your password';
        if (value !== formData.password) return 'Passwords do not match';
        return '';
      default:
        return '';
    }
  };
  
  const validateForm = (): boolean => {
    const newErrors: Partial<RegisterFormData> = {};
    Object.entries(formData).forEach(([name, value]) => {
      const error = validateField(name as keyof RegisterFormData, value);
      if (error) {
        newErrors[name as keyof RegisterFormData] = error;
      }
    });
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
    const error = validateField(name as keyof RegisterFormData, value);
    setErrors(prev => ({
      ...prev,
      [name]: error
    }));
    setServerError('');
  };
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    if (!validateForm()) {
      return;
    }
    
    setIsSubmitting(true);
    setServerError('');
    
    try {
      // 模拟 API 调用
      const response = await fetch('/api/register', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(formData)
      });
      
      if (!response.ok) {
        const errorData = await response.json();
        if (errorData.errors) {
          setErrors(errorData.errors);
        } else {
          setServerError('Registration failed. Please try again.');
        }
        return;
      }
      
      const data = await response.json();
      console.log('Registration successful:', data);
      navigate('/login');
    } catch (error) {
      setServerError('An error occurred. Please try again.');
    } finally {
      setIsSubmitting(false);
    }
  };
  
  const passwordStrength = getPasswordStrength(formData.password);
  
  return (
    <div className="register-form">
      <h1>Register</h1>
      {serverError && <p className="server-error">{serverError}</p>}
      <form onSubmit={handleSubmit}>
        <div className="form-row">
          <div className="form-group">
            <label htmlFor="firstName">First Name:</label>
            <input 
              type="text" 
              id="firstName"
              name="firstName"
              value={formData.firstName} 
              onChange={handleChange} 
              placeholder="Enter your first name"
              disabled={isSubmitting}
            />
            {errors.firstName && <p className="error-message">{errors.firstName}</p>}
          </div>
          <div className="form-group">
            <label htmlFor="lastName">Last Name:</label>
            <input 
              type="text" 
              id="lastName"
              name="lastName"
              value={formData.lastName} 
              onChange={handleChange} 
              placeholder="Enter your last name"
              disabled={isSubmitting}
            />
            {errors.lastName && <p className="error-message">{errors.lastName}</p>}
          </div>
        </div>
        <div className="form-group">
          <label htmlFor="email">Email:</label>
          <input 
            type="email" 
            id="email"
            name="email"
            value={formData.email} 
            onChange={handleChange} 
            placeholder="Enter your email"
            disabled={isSubmitting}
          />
          {errors.email && <p className="error-message">{errors.email}</p>}
        </div>
        <div className="form-group">
          <label htmlFor="password">Password:</label>
          <input 
            type="password" 
            id="password"
            name="password"
            value={formData.password} 
            onChange={handleChange} 
            placeholder="Enter your password"
            disabled={isSubmitting}
          />
          {errors.password && <p className="error-message">{errors.password}</p>}
          {formData.password && (
            <div className="password-strength">
              <div className={`strength-indicator ${passwordStrength}`}>
                {passwordStrength === 'weak' && 'Weak'}
                {passwordStrength === 'medium' && 'Medium'}
                {passwordStrength === 'strong' && 'Strong'}
              </div>
            </div>
          )}
        </div>
        <div className="form-group">
          <label htmlFor="confirmPassword">Confirm Password:</label>
          <input 
            type="password" 
            id="confirmPassword"
            name="confirmPassword"
            value={formData.confirmPassword} 
            onChange={handleChange} 
            placeholder="Confirm your password"
            disabled={isSubmitting}
          />
          {errors.confirmPassword && <p className="error-message">{errors.confirmPassword}</p>}
        </div>
        <div className="form-actions">
          <button 
            type="submit" 
            className="register-button"
            disabled={isSubmitting}
          >
            {isSubmitting ? 'Registering...' : 'Register'}
          </button>
        </div>
      </form>
      <div className="login-link">
        Already have an account? <a href="/login">Login</a>
      </div>
    </div>
  );
}

export default RegisterForm;

6.3 联系表单

功能需求

实现

import { useState } from 'react';

interface ContactFormData {
  name: string;
  email: string;
  subject: string;
  message: string;
}

function ContactForm() {
  const [formData, setFormData] = useState<ContactFormData>({
    name: '',
    email: '',
    subject: '',
    message: ''
  });
  const [errors, setErrors] = useState<Partial<ContactFormData>>({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [isSuccess, setIsSuccess] = useState(false);
  const [isError, setIsError] = useState(false);
  
  const validateField = (name: keyof ContactFormData, value: string): string => {
    switch (name) {
      case 'name':
        if (!value) return 'Name is required';
        if (value.length < 2) return 'Name must be at least 2 characters';
        return '';
      case 'email':
        if (!value) return 'Email is required';
        if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Please enter a valid email';
        return '';
      case 'subject':
        if (!value) return 'Subject is required';
        return '';
      case 'message':
        if (!value) return 'Message is required';
        if (value.length < 10) return 'Message must be at least 10 characters';
        return '';
      default:
        return '';
    }
  };
  
  const validateForm = (): boolean => {
    const newErrors: Partial<ContactFormData> = {};
    Object.entries(formData).forEach(([name, value]) => {
      const error = validateField(name as keyof ContactFormData, value);
      if (error) {
        newErrors[name as keyof ContactFormData] = error;
      }
    });
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
    const error = validateField(name as keyof ContactFormData, value);
    setErrors(prev => ({
      ...prev,
      [name]: error
    }));
    setIsSuccess(false);
    setIsError(false);
  };
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    if (!validateForm()) {
      return;
    }
    
    setIsSubmitting(true);
    setIsSuccess(false);
    setIsError(false);
    
    try {
      // 模拟 API 调用
      const response = await fetch('/api/contact', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(formData)
      });
      
      if (!response.ok) {
        throw new Error('Submission failed');
      }
      
      setIsSuccess(true);
      // 重置表单
      setFormData({
        name: '',
        email: '',
        subject: '',
        message: ''
      });
    } catch (error) {
      setIsError(true);
    } finally {
      setIsSubmitting(false);
    }
  };
  
  return (
    <div className="contact-form">
      <h1>Contact Us</h1>
      {isSuccess && (
        <div className="success-message">
          Thank you for your message! We'll get back to you soon.
        </div>
      )}
      {isError && (
        <div className="error-message">
          Oops! Something went wrong. Please try again later.
        </div>
      )}
      <form onSubmit={handleSubmit}>
        <div className="form-row">
          <div className="form-group">
            <label htmlFor="name">Name:</label>
            <input 
              type="text" 
              id="name"
              name="name"
              value={formData.name} 
              onChange={handleChange} 
              placeholder="Enter your name"
              disabled={isSubmitting}
            />
            {errors.name && <p className="field-error">{errors.name}</p>}
          </div>
          <div className="form-group">
            <label htmlFor="email">Email:</label>
            <input 
              type="email" 
              id="email"
              name="email"
              value={formData.email} 
              onChange={handleChange} 
              placeholder="Enter your email"
              disabled={isSubmitting}
            />
            {errors.email && <p className="field-error">{errors.email}</p>}
          </div>
        </div>
        <div className="form-group">
          <label htmlFor="subject">Subject:</label>
          <input 
            type="text" 
            id="subject"
            name="subject"
            value={formData.subject} 
            onChange={handleChange} 
            placeholder="Enter subject"
            disabled={isSubmitting}
          />
          {errors.subject && <p className="field-error">{errors.subject}</p>}
        </div>
        <div className="form-group">
          <label htmlFor="message">Message:</label>
          <textarea 
            id="message"
            name="message"
            value={formData.message} 
            onChange={handleChange} 
            placeholder="Enter your message"
            rows={5}
            disabled={isSubmitting}
          ></textarea>
          {errors.message && <p className="field-error">{errors.message}</p>}
        </div>
        <div className="form-actions">
          <button 
            type="submit" 
            className="submit-button"
            disabled={isSubmitting}
          >
            {isSubmitting ? 'Sending...' : 'Send Message'}
          </button>
        </div>
      </form>
    </div>
  );
}

export default ContactForm;

7. 最佳实践总结

7.1 表单设计

7.2 状态管理

7.3 验证策略

7.4 性能优化

7.5 安全性

7.6 可访问性

总结

React 表单处理是一个复杂但重要的话题,本文介绍了从基础到高级的各种表单处理技术:

  1. 表单基础:受控组件 vs 非受控组件
  2. 状态管理:从 useStateuseReducer
  3. 表单验证:客户端验证和服务端验证
  4. 性能优化:防抖、节流和虚拟化
  5. 第三方库:Formik、React Hook Form 和 Final Form
  6. 实战案例:登录、注册和联系表单
  7. 最佳实践:表单设计、状态管理、验证策略等

通过掌握这些技术和最佳实践,你可以构建出用户体验良好、性能优异、安全可靠的 React 表单。无论你是构建简单的联系表单还是复杂的多步骤表单,这些技术都能帮助你实现高质量的表单解决方案。


Edit page

Previous Post
React 可访问性最佳实践
Next Post
React 状态管理最佳实践