Skip to content

表单提交优化

概述

表单提交优化是提升用户体验的关键环节。从防止重复提交、到乐观更新、再到智能重试,合理的优化策略能够显著改善表单的响应速度和可靠性。本文将深入探讨各种表单提交优化技术和最佳实践。

防止重复提交

基础防重复提交

jsx
import { useState } from 'react';

function BasicPreventDuplicate() {
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    if (isSubmitting) {
      return; // 防止重复提交
    }
    
    setIsSubmitting(true);
    
    try {
      const formData = new FormData(e.target);
      await fetch('/api/submit', {
        method: 'POST',
        body: formData,
      });
      
      alert('提交成功!');
    } catch (error) {
      alert('提交失败: ' + error.message);
    } finally {
      setIsSubmitting(false);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input name="data" required />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '提交中...' : '提交'}
      </button>
    </form>
  );
}

使用useTransition

jsx
import { useTransition } from 'react';

function TransitionSubmit() {
  const [isPending, startTransition] = useTransition();
  const [result, setResult] = useState(null);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    startTransition(async () => {
      const formData = new FormData(e.target);
      const response = await fetch('/api/submit', {
        method: 'POST',
        body: formData,
      });
      
      const data = await response.json();
      setResult(data);
    });
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input name="data" required />
      <button type="submit" disabled={isPending}>
        {isPending ? '提交中...' : '提交'}
      </button>
      {result && <div>结果: {JSON.stringify(result)}</div>}
    </form>
  );
}

防抖提交

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

function DebouncedSubmit() {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const debounceTimerRef = useRef(null);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    // 清除之前的定时器
    if (debounceTimerRef.current) {
      clearTimeout(debounceTimerRef.current);
    }
    
    // 设置新的防抖定时器
    debounceTimerRef.current = setTimeout(async () => {
      setIsSubmitting(true);
      
      try {
        const formData = new FormData(e.target);
        await fetch('/api/submit', {
          method: 'POST',
          body: formData,
        });
        
        alert('提交成功!');
      } catch (error) {
        alert('提交失败: ' + error.message);
      } finally {
        setIsSubmitting(false);
      }
    }, 500); // 500ms防抖
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input name="data" required />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '提交中...' : '提交'}
      </button>
    </form>
  );
}

请求去重

jsx
import { useRef } from 'react';

function RequestDeduplication() {
  const abortControllerRef = useRef(null);
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    // 取消之前的请求
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }
    
    // 创建新的AbortController
    abortControllerRef.current = new AbortController();
    setIsSubmitting(true);
    
    try {
      const formData = new FormData(e.target);
      const response = await fetch('/api/submit', {
        method: 'POST',
        body: formData,
        signal: abortControllerRef.current.signal,
      });
      
      const data = await response.json();
      alert('提交成功: ' + JSON.stringify(data));
    } catch (error) {
      if (error.name !== 'AbortError') {
        alert('提交失败: ' + error.message);
      }
    } finally {
      setIsSubmitting(false);
      abortControllerRef.current = null;
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input name="data" required />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '提交中...' : '提交'}
      </button>
    </form>
  );
}

乐观更新

基础乐观更新

jsx
import { useState, useOptimistic } from 'react';

function OptimisticUpdate({ initialItems }) {
  const [items, setItems] = useState(initialItems);
  const [optimisticItems, addOptimisticItem] = useOptimistic(
    items,
    (state, newItem) => [...state, { ...newItem, pending: true }]
  );
  
  const handleAdd = async (e) => {
    e.preventDefault();
    
    const formData = new FormData(e.target);
    const text = formData.get('text');
    
    const newItem = {
      id: Date.now(),
      text,
      createdAt: new Date(),
    };
    
    // 乐观更新UI
    addOptimisticItem(newItem);
    
    // 重置表单
    e.target.reset();
    
    try {
      // 实际API调用
      const response = await fetch('/api/items', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newItem),
      });
      
      const savedItem = await response.json();
      
      // 更新实际数据
      setItems([...items, savedItem]);
    } catch (error) {
      // 失败时回滚
      console.error('添加失败:', error);
      alert('添加失败,请重试');
    }
  };
  
  return (
    <div>
      <form onSubmit={handleAdd}>
        <input name="text" placeholder="添加项目" required />
        <button type="submit">添加</button>
      </form>
      
      <ul>
        {optimisticItems.map(item => (
          <li key={item.id} className={item.pending ? 'pending' : ''}>
            {item.text}
            {item.pending && <span> (保存中...)</span>}
          </li>
        ))}
      </ul>
    </div>
  );
}

复杂乐观更新

jsx
function ComplexOptimisticUpdate() {
  const [posts, setPosts] = useState([]);
  const [optimisticPosts, updateOptimisticPosts] = useOptimistic(
    posts,
    (state, { type, post }) => {
      switch (type) {
        case 'add':
          return [...state, { ...post, optimistic: true }];
        
        case 'update':
          return state.map(p =>
            p.id === post.id ? { ...p, ...post, optimistic: true } : p
          );
        
        case 'delete':
          return state.filter(p => p.id !== post.id);
        
        case 'like':
          return state.map(p =>
            p.id === post.id ? { ...p, likes: p.likes + 1, optimistic: true } : p
          );
        
        default:
          return state;
      }
    }
  );
  
  const handleLike = async (postId) => {
    // 乐观更新
    updateOptimisticPosts({ type: 'like', post: { id: postId } });
    
    try {
      await fetch(`/api/posts/${postId}/like`, {
        method: 'POST',
      });
      
      // 更新实际数据
      setPosts(posts.map(p =>
        p.id === postId ? { ...p, likes: p.likes + 1 } : p
      ));
    } catch (error) {
      // 回滚
      setPosts(posts);
      alert('点赞失败');
    }
  };
  
  const handleDelete = async (postId) => {
    // 乐观更新
    updateOptimisticPosts({ type: 'delete', post: { id: postId } });
    
    try {
      await fetch(`/api/posts/${postId}`, {
        method: 'DELETE',
      });
      
      // 更新实际数据
      setPosts(posts.filter(p => p.id !== postId));
    } catch (error) {
      // 回滚
      setPosts(posts);
      alert('删除失败');
    }
  };
  
  return (
    <div>
      {optimisticPosts.map(post => (
        <article key={post.id} className={post.optimistic ? 'optimistic' : ''}>
          <h3>{post.title}</h3>
          <p>{post.content}</p>
          <button onClick={() => handleLike(post.id)}>
            ❤️ {post.likes}
          </button>
          <button onClick={() => handleDelete(post.id)}>
            删除
          </button>
        </article>
      ))}
    </div>
  );
}

智能重试

基础重试逻辑

jsx
import { useState } from 'react';

function RetrySubmit() {
  const [status, setStatus] = useState('idle');
  const [retryCount, setRetryCount] = useState(0);
  const maxRetries = 3;
  
  const submitWithRetry = async (formData, retriesLeft = maxRetries) => {
    try {
      const response = await fetch('/api/submit', {
        method: 'POST',
        body: formData,
      });
      
      if (!response.ok) {
        throw new Error('提交失败');
      }
      
      return await response.json();
    } catch (error) {
      if (retriesLeft > 0) {
        setRetryCount(maxRetries - retriesLeft + 1);
        
        // 指数退避
        const delay = Math.pow(2, maxRetries - retriesLeft) * 1000;
        await new Promise(resolve => setTimeout(resolve, delay));
        
        return submitWithRetry(formData, retriesLeft - 1);
      }
      
      throw error;
    }
  };
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setStatus('submitting');
    setRetryCount(0);
    
    try {
      const formData = new FormData(e.target);
      await submitWithRetry(formData);
      
      setStatus('success');
      alert('提交成功!');
    } catch (error) {
      setStatus('error');
      alert('提交失败,已重试' + maxRetries + '次');
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input name="data" required />
      <button type="submit" disabled={status === 'submitting'}>
        {status === 'submitting' 
          ? (retryCount > 0 ? `重试中 (${retryCount}/${maxRetries})...` : '提交中...')
          : '提交'
        }
      </button>
    </form>
  );
}

高级重试策略

jsx
class RetryStrategy {
  constructor(options = {}) {
    this.maxRetries = options.maxRetries || 3;
    this.baseDelay = options.baseDelay || 1000;
    this.maxDelay = options.maxDelay || 30000;
    this.retryableErrors = options.retryableErrors || [500, 502, 503, 504];
  }
  
  shouldRetry(error, attempt) {
    if (attempt >= this.maxRetries) {
      return false;
    }
    
    // 网络错误总是重试
    if (error.name === 'TypeError' || error.message === 'Failed to fetch') {
      return true;
    }
    
    // HTTP错误根据状态码判断
    if (error.status) {
      return this.retryableErrors.includes(error.status);
    }
    
    return false;
  }
  
  getDelay(attempt) {
    // 指数退避 + 抖动
    const exponentialDelay = Math.min(
      this.baseDelay * Math.pow(2, attempt),
      this.maxDelay
    );
    
    const jitter = Math.random() * 0.3 * exponentialDelay;
    
    return exponentialDelay + jitter;
  }
}

function AdvancedRetrySubmit() {
  const [status, setStatus] = useState({
    state: 'idle',
    attempt: 0,
    error: null,
  });
  
  const retryStrategy = useMemo(() => new RetryStrategy({
    maxRetries: 3,
    baseDelay: 1000,
    retryableErrors: [500, 502, 503, 504],
  }), []);
  
  const submitWithRetry = async (formData, attempt = 0) => {
    try {
      const response = await fetch('/api/submit', {
        method: 'POST',
        body: formData,
      });
      
      if (!response.ok) {
        const error = new Error('提交失败');
        error.status = response.status;
        throw error;
      }
      
      return await response.json();
    } catch (error) {
      if (retryStrategy.shouldRetry(error, attempt)) {
        const delay = retryStrategy.getDelay(attempt);
        
        setStatus({
          state: 'retrying',
          attempt: attempt + 1,
          error: error.message,
        });
        
        await new Promise(resolve => setTimeout(resolve, delay));
        
        return submitWithRetry(formData, attempt + 1);
      }
      
      throw error;
    }
  };
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setStatus({ state: 'submitting', attempt: 0, error: null });
    
    try {
      const formData = new FormData(e.target);
      await submitWithRetry(formData);
      
      setStatus({ state: 'success', attempt: 0, error: null });
    } catch (error) {
      setStatus({
        state: 'error',
        attempt: status.attempt,
        error: error.message,
      });
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input name="data" required />
      
      <button
        type="submit"
        disabled={status.state === 'submitting' || status.state === 'retrying'}
      >
        {status.state === 'submitting' && '提交中...'}
        {status.state === 'retrying' && `重试中 (${status.attempt}/3)...`}
        {status.state === 'idle' && '提交'}
        {status.state === 'success' && '提交成功'}
        {status.state === 'error' && '重试'}
      </button>
      
      {status.error && (
        <div className="error">
          {status.error}
          {status.attempt > 0 && ` (已重试${status.attempt}次)`}
        </div>
      )}
    </form>
  );
}

批量提交

批量操作

jsx
import { useState } from 'react';

function BatchSubmit({ items }) {
  const [selectedIds, setSelectedIds] = useState(new Set());
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [progress, setProgress] = useState(0);
  
  const toggleSelection = (id) => {
    const newSelection = new Set(selectedIds);
    if (newSelection.has(id)) {
      newSelection.delete(id);
    } else {
      newSelection.add(id);
    }
    setSelectedIds(newSelection);
  };
  
  const handleBatchSubmit = async () => {
    const selectedItems = Array.from(selectedIds);
    setIsSubmitting(true);
    setProgress(0);
    
    const total = selectedItems.length;
    let completed = 0;
    
    // 并发控制
    const concurrency = 3;
    const queue = [...selectedItems];
    const results = [];
    
    const processItem = async (id) => {
      try {
        const response = await fetch(`/api/items/${id}`, {
          method: 'POST',
        });
        
        const result = await response.json();
        results.push({ id, success: true, result });
      } catch (error) {
        results.push({ id, success: false, error: error.message });
      } finally {
        completed++;
        setProgress(Math.round((completed / total) * 100));
      }
    };
    
    // 并发执行
    while (queue.length > 0 || completed < total) {
      const batch = [];
      
      for (let i = 0; i < concurrency && queue.length > 0; i++) {
        const id = queue.shift();
        batch.push(processItem(id));
      }
      
      if (batch.length > 0) {
        await Promise.all(batch);
      }
    }
    
    setIsSubmitting(false);
    
    // 显示结果
    const successCount = results.filter(r => r.success).length;
    alert(`完成! 成功: ${successCount}/${total}`);
    
    // 清空选择
    setSelectedIds(new Set());
  };
  
  return (
    <div>
      <div className="actions">
        <button
          onClick={handleBatchSubmit}
          disabled={selectedIds.size === 0 || isSubmitting}
        >
          批量提交 ({selectedIds.size})
        </button>
        
        {isSubmitting && (
          <div className="progress">
            <div className="progress-bar" style={{ width: `${progress}%` }} />
            <span>{progress}%</span>
          </div>
        )}
      </div>
      
      <ul>
        {items.map(item => (
          <li key={item.id}>
            <label>
              <input
                type="checkbox"
                checked={selectedIds.has(item.id)}
                onChange={() => toggleSelection(item.id)}
                disabled={isSubmitting}
              />
              {item.name}
            </label>
          </li>
        ))}
      </ul>
    </div>
  );
}

队列管理

提交队列

jsx
import { useState, useRef, useCallback } from 'react';

class SubmitQueue {
  constructor(options = {}) {
    this.queue = [];
    this.processing = false;
    this.concurrency = options.concurrency || 1;
    this.onProgress = options.onProgress || (() => {});
  }
  
  add(task) {
    return new Promise((resolve, reject) => {
      this.queue.push({ task, resolve, reject });
      this.process();
    });
  }
  
  async process() {
    if (this.processing || this.queue.length === 0) {
      return;
    }
    
    this.processing = true;
    
    while (this.queue.length > 0) {
      const batch = this.queue.splice(0, this.concurrency);
      
      this.onProgress({
        remaining: this.queue.length + batch.length,
        processing: batch.length,
      });
      
      await Promise.allSettled(
        batch.map(async ({ task, resolve, reject }) => {
          try {
            const result = await task();
            resolve(result);
          } catch (error) {
            reject(error);
          }
        })
      );
    }
    
    this.processing = false;
    this.onProgress({ remaining: 0, processing: 0 });
  }
}

function QueuedSubmit() {
  const [queueStatus, setQueueStatus] = useState({
    remaining: 0,
    processing: 0,
  });
  
  const queueRef = useRef(null);
  
  if (!queueRef.current) {
    queueRef.current = new SubmitQueue({
      concurrency: 3,
      onProgress: setQueueStatus,
    });
  }
  
  const handleSubmit = useCallback(async (formData) => {
    return queueRef.current.add(async () => {
      const response = await fetch('/api/submit', {
        method: 'POST',
        body: formData,
      });
      
      if (!response.ok) {
        throw new Error('提交失败');
      }
      
      return await response.json();
    });
  }, []);
  
  const handleFormSubmit = async (e) => {
    e.preventDefault();
    
    const formData = new FormData(e.target);
    
    try {
      const result = await handleSubmit(formData);
      console.log('提交成功:', result);
      e.target.reset();
    } catch (error) {
      alert('提交失败: ' + error.message);
    }
  };
  
  return (
    <div>
      {queueStatus.remaining > 0 && (
        <div className="queue-status">
          队列中: {queueStatus.remaining} | 处理中: {queueStatus.processing}
        </div>
      )}
      
      <form onSubmit={handleFormSubmit}>
        <input name="data" required />
        <button type="submit">提交到队列</button>
      </form>
    </div>
  );
}

离线支持

离线队列

jsx
import { useState, useEffect } from 'react';

class OfflineQueue {
  constructor() {
    this.storageKey = 'offline-queue';
    this.queue = this.loadQueue();
  }
  
  loadQueue() {
    try {
      const stored = localStorage.getItem(this.storageKey);
      return stored ? JSON.parse(stored) : [];
    } catch {
      return [];
    }
  }
  
  saveQueue() {
    try {
      localStorage.setItem(this.storageKey, JSON.stringify(this.queue));
    } catch (error) {
      console.error('保存队列失败:', error);
    }
  }
  
  add(data) {
    this.queue.push({
      id: Date.now(),
      data,
      timestamp: new Date().toISOString(),
    });
    this.saveQueue();
  }
  
  async processQueue() {
    if (!navigator.onLine || this.queue.length === 0) {
      return;
    }
    
    const processed = [];
    
    for (const item of this.queue) {
      try {
        await fetch('/api/submit', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(item.data),
        });
        
        processed.push(item.id);
      } catch (error) {
        console.error('处理失败:', error);
        break;
      }
    }
    
    this.queue = this.queue.filter(item => !processed.includes(item.id));
    this.saveQueue();
  }
  
  getQueue() {
    return this.queue;
  }
}

function OfflineSubmit() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);
  const [queue, setQueue] = useState([]);
  const queueRef = useRef(new OfflineQueue());
  
  useEffect(() => {
    const handleOnline = () => {
      setIsOnline(true);
      queueRef.current.processQueue().then(() => {
        setQueue(queueRef.current.getQueue());
      });
    };
    
    const handleOffline = () => {
      setIsOnline(false);
    };
    
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    
    // 初始加载队列
    setQueue(queueRef.current.getQueue());
    
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    const formData = new FormData(e.target);
    const data = Object.fromEntries(formData);
    
    if (isOnline) {
      try {
        await fetch('/api/submit', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(data),
        });
        
        alert('提交成功!');
        e.target.reset();
      } catch (error) {
        // 在线但请求失败,添加到队列
        queueRef.current.add(data);
        setQueue(queueRef.current.getQueue());
        alert('提交失败,已添加到离线队列');
      }
    } else {
      // 离线,直接添加到队列
      queueRef.current.add(data);
      setQueue(queueRef.current.getQueue());
      alert('当前离线,已添加到队列,将在恢复网络后自动提交');
      e.target.reset();
    }
  };
  
  return (
    <div>
      <div className="status-bar">
        状态: {isOnline ? '在线' : '离线'}
        {queue.length > 0 && ` | 队列中有 ${queue.length} 项待提交`}
      </div>
      
      <form onSubmit={handleSubmit}>
        <input name="title" placeholder="标题" required />
        <textarea name="content" placeholder="内容" required />
        <button type="submit">提交</button>
      </form>
      
      {queue.length > 0 && (
        <div className="queue-list">
          <h3>离线队列</h3>
          <ul>
            {queue.map(item => (
              <li key={item.id}>
                {item.data.title} - {new Date(item.timestamp).toLocaleString()}
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
}

总结

表单提交优化要点:

  1. 防重复提交:禁用按钮、请求去重、防抖
  2. 乐观更新:提升用户体验,及时回滚
  3. 智能重试:指数退避、抖动、可配置策略
  4. 批量处理:并发控制、进度反馈
  5. 队列管理:任务队列、优先级调度
  6. 离线支持:离线队列、自动同步

合理的提交优化策略能够显著提升表单的可靠性和用户体验。