Appearance
表单提交优化
概述
表单提交优化是提升用户体验的关键环节。从防止重复提交、到乐观更新、再到智能重试,合理的优化策略能够显著改善表单的响应速度和可靠性。本文将深入探讨各种表单提交优化技术和最佳实践。
防止重复提交
基础防重复提交
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>
);
}总结
表单提交优化要点:
- 防重复提交:禁用按钮、请求去重、防抖
- 乐观更新:提升用户体验,及时回滚
- 智能重试:指数退避、抖动、可配置策略
- 批量处理:并发控制、进度反馈
- 队列管理:任务队列、优先级调度
- 离线支持:离线队列、自动同步
合理的提交优化策略能够显著提升表单的可靠性和用户体验。