Appearance
乐观更新错误回滚
学习目标
通过本章学习,你将掌握:
- 错误回滚机制
- 自动回滚原理
- 手动回滚处理
- 错误提示策略
- 重试机制
- 部分回滚
- 复杂场景处理
- 用户体验优化
第一部分:自动回滚机制
1.1 useOptimistic的自动回滚
jsx
'use client';
import { useOptimistic, useState } from 'react';
import { likePost } from './actions';
export default function LikeButton({ postId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const [optimisticLikes, setOptimisticLikes] = useOptimistic(
likes,
(current, increment) => current + increment
);
const handleLike = async () => {
// 步骤1:乐观更新(立即显示)
setOptimisticLikes(1); // likes + 1
try {
// 步骤2:发送请求
const newLikes = await likePost(postId);
// 步骤3:成功 - 更新实际状态
setLikes(newLikes);
// 此时optimisticLikes会自动同步到newLikes
} catch (error) {
// 步骤4:失败 - 自动回滚
// optimisticLikes自动恢复到likes的值
// 无需手动处理!
console.error('点赞失败:', error);
}
};
return (
<button onClick={handleLike}>
❤️ {optimisticLikes}
</button>
);
}
// 工作原理:
// 成功:optimisticLikes跟随likes更新
// 失败:optimisticLikes自动恢复到likes1.2 回滚时机
jsx
'use client';
import { useOptimistic, useState, useEffect } from 'react';
export default function AutoRollbackDemo() {
const [value, setValue] = useState(0);
const [logs, setLogs] = useState([]);
const [optimisticValue, setOptimisticValue] = useOptimistic(
value,
(_, newValue) => newValue
);
const addLog = (message) => {
setLogs(prev => [...prev, `${new Date().toLocaleTimeString()}: ${message}`]);
};
const handleUpdate = async (shouldFail) => {
addLog(`乐观更新: ${value} → ${value + 1}`);
setOptimisticValue(value + 1);
// 模拟异步操作
await new Promise(resolve => setTimeout(resolve, 1000));
if (shouldFail) {
addLog('操作失败,自动回滚');
// 抛出错误触发回滚
throw new Error('操作失败');
} else {
addLog(`操作成功,确认更新: ${value + 1}`);
setValue(value + 1);
}
};
return (
<div>
<p>实际值: {value}</p>
<p>显示值: {optimisticValue}</p>
<button onClick={() => handleUpdate(false).catch(() => {})}>
成功更新
</button>
<button onClick={() => handleUpdate(true).catch(() => {})}>
失败回滚
</button>
<div className="logs">
{logs.map((log, i) => (
<div key={i}>{log}</div>
))}
</div>
</div>
);
}1.3 多次乐观更新
jsx
'use client';
import { useOptimistic, useState } from 'react';
export default function MultipleOptimisticUpdates() {
const [count, setCount] = useState(0);
const [pending, setPending] = useState(0);
const [optimisticCount, addOptimistic] = useOptimistic(
count,
(current, increment) => current + increment
);
const handleIncrement = async () => {
setPending(prev => prev + 1);
// 乐观更新
addOptimistic(1);
try {
await new Promise((resolve, reject) => {
setTimeout(() => {
// 50%概率失败
if (Math.random() > 0.5) {
resolve();
} else {
reject(new Error('随机失败'));
}
}, 1000);
});
// 成功
setCount(prev => prev + 1);
} catch (error) {
// 失败 - 自动回滚
console.error('失败,已回滚');
} finally {
setPending(prev => prev - 1);
}
};
return (
<div>
<p>计数: {optimisticCount}</p>
<p>待确认: {pending}</p>
<button onClick={handleIncrement}>
增加(可能失败)
</button>
</div>
);
}
// 说明:
// - 多次点击会创建多个乐观更新
// - 每个更新独立处理
// - 失败的会各自回滚
// - 成功的会保留1.4 回滚动画效果
jsx
'use client';
import { useOptimistic, useState } from 'react';
import { motion } from 'framer-motion';
export default function AnimatedRollback({ postId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const [isRollingBack, setIsRollingBack] = useState(false);
const [optimisticLikes, setOptimisticLikes] = useOptimistic(
likes,
(current, increment) => current + increment
);
const handleLike = async () => {
setOptimisticLikes(1);
try {
const newLikes = await likePost(postId);
setLikes(newLikes);
} catch (error) {
// 触发回滚动画
setIsRollingBack(true);
// 动画结束后清除状态
setTimeout(() => setIsRollingBack(false), 500);
}
};
return (
<motion.button
onClick={handleLike}
animate={isRollingBack ? {
x: [0, -10, 10, -10, 10, 0],
rotate: [0, -5, 5, -5, 5, 0]
} : {}}
transition={{ duration: 0.5 }}
className={isRollingBack ? 'rolling-back' : ''}
>
❤️ {optimisticLikes}
</motion.button>
);
}
// CSS
/*
.rolling-back {
background-color: #ffebee;
color: #c62828;
}
*/1.5 回滚状态追踪
jsx
'use client';
import { useOptimistic, useState, useRef } from 'react';
export default function RollbackTracking({ initialData }) {
const [data, setData] = useState(initialData);
const [rollbackHistory, setRollbackHistory] = useState([]);
const attemptId = useRef(0);
const [optimisticData, setOptimisticData] = useOptimistic(
data,
(current, update) => ({ ...current, ...update })
);
const handleUpdate = async (updates) => {
const currentAttemptId = ++attemptId.current;
const timestamp = new Date();
// 乐观更新
setOptimisticData(updates);
try {
const result = await updateData(updates);
setData(result);
// 记录成功
setRollbackHistory(prev => [...prev, {
id: currentAttemptId,
timestamp,
status: 'success',
updates
}]);
} catch (error) {
// 记录回滚
setRollbackHistory(prev => [...prev, {
id: currentAttemptId,
timestamp,
status: 'rollback',
updates,
error: error.message
}]);
}
};
return (
<div>
<div className="data-display">
<p>当前值: {optimisticData.value}</p>
<button onClick={() => handleUpdate({ value: optimisticData.value + 1 })}>
增加
</button>
</div>
<div className="history">
<h3>操作历史</h3>
{rollbackHistory.slice().reverse().map(record => (
<div
key={record.id}
className={`history-item ${record.status}`}
>
<span className="time">
{record.timestamp.toLocaleTimeString()}
</span>
<span className="status">
{record.status === 'success' ? '✓ 成功' : '✗ 回滚'}
</span>
{record.error && (
<span className="error">{record.error}</span>
)}
</div>
))}
</div>
</div>
);
}第二部分:错误提示
2.1 显示错误消息
jsx
'use client';
import { useOptimistic, useState } from 'react';
import { updateTodo } from './actions';
export default function TodoWithErrorMessage({ todo }) {
const [completed, setCompleted] = useState(todo.completed);
const [error, setError] = useState(null);
const [optimisticCompleted, setOptimisticCompleted] = useOptimistic(
completed,
(_, newValue) => newValue
);
const handleToggle = async () => {
setError(null);
// 乐观更新
setOptimisticCompleted(!completed);
try {
await updateTodo(todo.id, !completed);
setCompleted(!completed);
} catch (error) {
// 回滚后显示错误
setError(error.message);
// 3秒后清除错误消息
setTimeout(() => setError(null), 3000);
}
};
return (
<div>
<div className="todo-item">
<input
type="checkbox"
checked={optimisticCompleted}
onChange={handleToggle}
/>
<span>{todo.text}</span>
</div>
{error && (
<div className="error-toast">
{error}
</div>
)}
</div>
);
}2.2 内联错误提示
jsx
'use client';
import { useOptimistic, useState } from 'react';
export default function InlineError({ postId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const [error, setError] = useState(null);
const [optimisticLikes, setOptimisticLikes] = useOptimistic(
likes,
(current, increment) => current + increment
);
const handleLike = async () => {
setError(null);
setOptimisticLikes(1);
try {
const newLikes = await likePost(postId);
setLikes(newLikes);
} catch (error) {
setError('点赞失败,请重试');
}
};
return (
<div className="like-button-wrapper">
<button onClick={handleLike} className={error ? 'error' : ''}>
❤️ {optimisticLikes}
</button>
{error && (
<span className="error-message">
{error}
</span>
)}
</div>
);
}
/* CSS */
.like-button-wrapper {
position: relative;
}
.like-button.error {
animation: shake 0.5s;
}
.error-message {
position: absolute;
bottom: -25px;
left: 0;
color: red;
font-size: 12px;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}2.3 Toast通知
jsx
'use client';
import { useOptimistic, useState } from 'react';
import { toast } from 'react-hot-toast';
export default function TodoWithToast({ todo }) {
const [completed, setCompleted] = useState(todo.completed);
const [optimisticCompleted, setOptimisticCompleted] = useOptimistic(
completed,
(_, newValue) => newValue
);
const handleToggle = async () => {
// 乐观更新
setOptimisticCompleted(!completed);
// 显示加载Toast
const toastId = toast.loading('更新中...');
try {
await updateTodo(todo.id, !completed);
setCompleted(!completed);
// 成功Toast
toast.success('已更新!', { id: toastId });
} catch (error) {
// 失败Toast
toast.error('更新失败,请重试', { id: toastId });
}
};
return (
<div>
<input
type="checkbox"
checked={optimisticCompleted}
onChange={handleToggle}
/>
<span>{todo.text}</span>
</div>
);
}2.4 错误分类提示
jsx
'use client';
import { useOptimistic, useState } from 'react';
export default function CategorizedErrors({ initialData }) {
const [data, setData] = useState(initialData);
const [error, setError] = useState(null);
const [optimisticData, setOptimisticData] = useOptimistic(
data,
(current, updates) => ({ ...current, ...updates })
);
const getErrorMessage = (error) => {
if (error.code === 'NETWORK_ERROR') {
return {
title: '网络错误',
message: '网络连接失败,请检查您的网络设置',
retry: true
};
}
if (error.code === 'VALIDATION_ERROR') {
return {
title: '验证失败',
message: error.message,
retry: false
};
}
if (error.code === 'PERMISSION_DENIED') {
return {
title: '权限不足',
message: '您没有权限执行此操作',
retry: false
};
}
return {
title: '操作失败',
message: '发生未知错误,请稍后重试',
retry: true
};
};
const handleUpdate = async (updates) => {
setError(null);
setOptimisticData(updates);
try {
const result = await updateData(updates);
setData(result);
} catch (err) {
const errorInfo = getErrorMessage(err);
setError(errorInfo);
}
};
return (
<div>
<div className="data-editor">
<input
value={optimisticData.value}
onChange={(e) => handleUpdate({ value: e.target.value })}
/>
</div>
{error && (
<div className="error-notification">
<div className="error-header">
<strong>{error.title}</strong>
</div>
<p>{error.message}</p>
{error.retry && (
<button onClick={() => handleUpdate(optimisticData)}>
重试
</button>
)}
</div>
)}
</div>
);
}2.5 错误统计与监控
jsx
'use client';
import { useOptimistic, useState, useEffect } from 'react';
export default function ErrorMonitoring({ initialData }) {
const [data, setData] = useState(initialData);
const [errorStats, setErrorStats] = useState({
total: 0,
network: 0,
validation: 0,
server: 0,
lastError: null
});
const [optimisticData, setOptimisticData] = useOptimistic(
data,
(current, updates) => ({ ...current, ...updates })
);
const recordError = (error) => {
setErrorStats(prev => ({
...prev,
total: prev.total + 1,
[error.type]: (prev[error.type] || 0) + 1,
lastError: {
type: error.type,
message: error.message,
timestamp: new Date()
}
}));
// 发送错误到监控服务
if (typeof window !== 'undefined' && window.analytics) {
window.analytics.track('OptimisticUpdateFailed', {
errorType: error.type,
errorMessage: error.message
});
}
};
const handleUpdate = async (updates) => {
setOptimisticData(updates);
try {
const result = await updateData(updates);
setData(result);
} catch (error) {
recordError({
type: error.type || 'unknown',
message: error.message
});
}
};
return (
<div>
<div className="error-stats">
<h3>错误统计</h3>
<p>总错误数: {errorStats.total}</p>
<p>网络错误: {errorStats.network}</p>
<p>验证错误: {errorStats.validation}</p>
<p>服务器错误: {errorStats.server}</p>
{errorStats.lastError && (
<div className="last-error">
<h4>最近错误</h4>
<p>类型: {errorStats.lastError.type}</p>
<p>消息: {errorStats.lastError.message}</p>
<p>时间: {errorStats.lastError.timestamp.toLocaleString()}</p>
</div>
)}
</div>
{/* 数据编辑UI */}
</div>
);
}第三部分:重试机制
3.1 自动重试
jsx
'use client';
import { useOptimistic, useState } from 'react';
export default function AutoRetry({ postId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const [retryCount, setRetryCount] = useState(0);
const [optimisticLikes, setOptimisticLikes] = useOptimistic(
likes,
(current, increment) => current + increment
);
const likeWithRetry = async (retries = 3) => {
setOptimisticLikes(1);
for (let i = 0; i <= retries; i++) {
try {
const newLikes = await likePost(postId);
setLikes(newLikes);
setRetryCount(0);
return; // 成功
} catch (error) {
if (i === retries) {
// 最后一次重试也失败
setRetryCount(0);
alert('点赞失败,请稍后再试');
// 回滚会自动发生
return;
}
// 继续重试
setRetryCount(i + 1);
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
};
return (
<div>
<button onClick={() => likeWithRetry()}>
❤️ {optimisticLikes}
</button>
{retryCount > 0 && (
<span className="retrying">
重试中 ({retryCount}/3)...
</span>
)}
</div>
);
}3.2 手动重试
jsx
'use client';
import { useOptimistic, useState } from 'react';
export default function ManualRetry({ postId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const [error, setError] = useState(null);
const [pending, setPending] = useState(false);
const [optimisticLikes, setOptimisticLikes] = useOptimistic(
likes,
(current, increment) => current + increment
);
const attemptLike = async () => {
setError(null);
setPending(true);
setOptimisticLikes(1);
try {
const newLikes = await likePost(postId);
setLikes(newLikes);
} catch (error) {
setError('点赞失败');
// 回滚会自动发生
} finally {
setPending(false);
}
};
return (
<div>
<button onClick={attemptLike} disabled={pending}>
❤️ {optimisticLikes}
</button>
{error && (
<div className="error-box">
<span>{error}</span>
<button onClick={attemptLike}>重试</button>
</div>
)}
</div>
);
}3.3 指数退避重试
jsx
'use client';
import { useOptimistic, useState } from 'react';
async function retryWithBackoff(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) {
throw error;
}
// 指数退避:1s, 2s, 4s
const delay = Math.pow(2, i) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
export default function ExponentialBackoff({ postId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const [status, setStatus] = useState('idle');
const [optimisticLikes, setOptimisticLikes] = useOptimistic(
likes,
(current, increment) => current + increment
);
const handleLike = async () => {
setStatus('pending');
setOptimisticLikes(1);
try {
const newLikes = await retryWithBackoff(() => likePost(postId));
setLikes(newLikes);
setStatus('success');
} catch (error) {
setStatus('error');
setTimeout(() => setStatus('idle'), 3000);
}
};
return (
<div>
<button onClick={handleLike} disabled={status === 'pending'}>
❤️ {optimisticLikes}
</button>
{status === 'pending' && <span>重试中...</span>}
{status === 'error' && <span className="error">所有重试均失败</span>}
</div>
);
}3.4 智能重试策略
jsx
'use client';
import { useOptimistic, useState, useRef } from 'react';
class RetryStrategy {
constructor(maxRetries = 3, baseDelay = 1000) {
this.maxRetries = maxRetries;
this.baseDelay = baseDelay;
}
async execute(fn, onRetry) {
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === this.maxRetries - 1) {
throw error;
}
// 根据错误类型决定是否重试
if (!this.shouldRetry(error)) {
throw error;
}
const delay = this.getDelay(attempt, error);
if (onRetry) {
onRetry(attempt + 1, delay);
}
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
shouldRetry(error) {
// 网络错误和超时错误应该重试
return error.code === 'NETWORK_ERROR' ||
error.code === 'TIMEOUT' ||
error.status >= 500;
}
getDelay(attempt, error) {
// 服务器错误使用指数退避
if (error.status >= 500) {
return Math.pow(2, attempt) * this.baseDelay;
}
// 网络错误使用固定延迟
return this.baseDelay;
}
}
export default function SmartRetry({ initialData }) {
const [data, setData] = useState(initialData);
const [retryInfo, setRetryInfo] = useState(null);
const retryStrategy = useRef(new RetryStrategy(3, 1000));
const [optimisticData, setOptimisticData] = useOptimistic(
data,
(current, updates) => ({ ...current, ...updates })
);
const handleUpdate = async (updates) => {
setOptimisticData(updates);
setRetryInfo(null);
try {
const result = await retryStrategy.current.execute(
() => updateData(updates),
(attempt, delay) => {
setRetryInfo({
attempt,
delay,
message: `第 ${attempt} 次重试,${delay}ms 后执行`
});
}
);
setData(result);
setRetryInfo(null);
} catch (error) {
toast.error('操作失败:' + error.message);
}
};
return (
<div>
<input
value={optimisticData.value}
onChange={(e) => handleUpdate({ value: e.target.value })}
/>
{retryInfo && (
<div className="retry-info">
{retryInfo.message}
</div>
)}
</div>
);
}3.5 重试队列管理
jsx
'use client';
import { useOptimistic, useState, useRef } from 'react';
class RetryQueue {
constructor() {
this.queue = [];
this.processing = false;
}
async add(task) {
this.queue.push(task);
if (!this.processing) {
await this.process();
}
}
async process() {
this.processing = true;
while (this.queue.length > 0) {
const task = this.queue.shift();
try {
await task.execute();
} catch (error) {
// 失败的任务重新加入队列
if (task.retries < task.maxRetries) {
task.retries++;
this.queue.push(task);
// 等待一段时间再重试
await new Promise(resolve =>
setTimeout(resolve, 1000 * task.retries)
);
} else {
task.onError(error);
}
}
}
this.processing = false;
}
}
export default function QueuedRetry({ initialTodos }) {
const [todos, setTodos] = useState(initialTodos);
const retryQueue = useRef(new RetryQueue());
const [optimisticTodos, updateOptimistic] = useOptimistic(
todos,
(state, action) => {
if (action.type === 'toggle') {
return state.map(todo =>
todo.id === action.id
? { ...todo, completed: !todo.completed }
: todo
);
}
return state;
}
);
const handleToggle = (id) => {
const todo = todos.find(t => t.id === id);
// 立即乐观更新
updateOptimistic({ type: 'toggle', id });
// 加入重试队列
retryQueue.current.add({
execute: async () => {
await updateTodo(id, !todo.completed);
setTodos(prev =>
prev.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
)
);
},
retries: 0,
maxRetries: 3,
onError: (error) => {
toast.error(`更新 "${todo.text}" 失败`);
}
});
};
return (
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggle(todo.id)}
/>
<span>{todo.text}</span>
</li>
))}
</ul>
);
}第四部分:部分回滚
4.1 列表部分回滚
jsx
'use client';
import { useOptimistic, useState } from 'react';
export default function PartialRollback({ initialTodos }) {
const [todos, setTodos] = useState(initialTodos);
const [optimisticTodos, updateOptimistic] = useOptimistic(
todos,
(state, { id, completed }) =>
state.map(todo =>
todo.id === id ? { ...todo, completed, pending: true } : todo
)
);
const handleToggle = async (id) => {
const todo = todos.find(t => t.id === id);
// 乐观更新单个todo
updateOptimistic({ id, completed: !todo.completed });
try {
await updateTodo(id, !todo.completed);
// 成功:更新实际状态
setTodos(prev =>
prev.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
)
);
} catch (error) {
// 失败:只回滚这一个todo
// useOptimistic会自动处理
alert(`更新 "${todo.text}" 失败`);
}
};
return (
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} className={todo.pending ? 'pending' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggle(todo.id)}
/>
<span>{todo.text}</span>
</li>
))}
</ul>
);
}4.2 批量操作部分成功
jsx
'use client';
import { useOptimistic, useState } from 'react';
export default function BatchPartialSuccess({ initialTodos }) {
const [todos, setTodos] = useState(initialTodos);
const [selectedIds, setSelectedIds] = useState([]);
const [failures, setFailures] = useState([]);
const [optimisticTodos, updateOptimistic] = useOptimistic(
todos,
(state, completedIds) =>
state.map(todo =>
completedIds.includes(todo.id)
? { ...todo, completed: true, pending: true }
: todo
)
);
const handleBatchComplete = async () => {
if (selectedIds.length === 0) return;
// 乐观更新所有选中的
updateOptimistic(selectedIds);
try {
// 批量处理(可能部分失败)
const result = await batchUpdateTodos(selectedIds);
// 更新成功的
setTodos(prev =>
prev.map(todo =>
result.successful.includes(todo.id)
? { ...todo, completed: true }
: todo
)
);
// 记录失败的
setFailures(result.failed);
if (result.failed.length > 0) {
// 失败的会自动回滚
alert(`${result.failed.length} 项更新失败`);
}
// 清空选择
setSelectedIds([]);
} catch (error) {
alert('批量操作失败');
setFailures([]);
}
};
return (
<div>
<button onClick={handleBatchComplete}>
完成选中的 {selectedIds.length} 项
</button>
{failures.length > 0 && (
<div className="error">
以下项目更新失败:
{failures.map(id => {
const todo = todos.find(t => t.id === id);
return <div key={id}>{todo?.text}</div>;
})}
</div>
)}
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={selectedIds.includes(todo.id)}
onChange={(e) => {
if (e.target.checked) {
setSelectedIds(prev => [...prev, todo.id]);
} else {
setSelectedIds(prev => prev.filter(id => id !== todo.id));
}
}}
/>
<span className={todo.completed ? 'completed' : ''}>
{todo.text}
</span>
{todo.pending && <span>确认中...</span>}
</li>
))}
</ul>
</div>
);
}4.3 嵌套数据部分回滚
jsx
'use client';
import { useOptimistic, useState } from 'react';
export default function NestedPartialRollback({ initialPost }) {
const [post, setPost] = useState(initialPost);
const [optimisticPost, updateOptimistic] = useOptimistic(
post,
(current, action) => {
if (action.type === 'likeComment') {
return {
...current,
comments: current.comments.map(comment =>
comment.id === action.commentId
? {
...comment,
likes: comment.likes + 1,
pending: true
}
: comment
)
};
}
return current;
}
);
const handleLikeComment = async (commentId) => {
// 乐观更新评论点赞
updateOptimistic({ type: 'likeComment', commentId });
try {
const result = await likeComment(post.id, commentId);
// 成功:更新整个文章数据
setPost(result);
} catch (error) {
// 失败:只回滚这个评论的点赞
toast.error('点赞失败');
}
};
return (
<article>
<h2>{optimisticPost.title}</h2>
<p>{optimisticPost.content}</p>
<section className="comments">
{optimisticPost.comments.map(comment => (
<div
key={comment.id}
className={comment.pending ? 'pending' : ''}
>
<p>{comment.text}</p>
<button onClick={() => handleLikeComment(comment.id)}>
❤️ {comment.likes}
</button>
</div>
))}
</section>
</article>
);
}4.4 分组回滚策略
jsx
'use client';
import { useOptimistic, useState } from 'react';
export default function GroupedRollback({ initialItems }) {
const [items, setItems] = useState(initialItems);
const [groupErrors, setGroupErrors] = useState({});
const [optimisticItems, updateOptimistic] = useOptimistic(
items,
(state, action) => {
if (action.type === 'updateGroup') {
return state.map(item =>
item.group === action.group
? { ...item, ...action.updates, pending: true }
: item
);
}
return state;
}
);
const handleUpdateGroup = async (group, updates) => {
// 乐观更新整组
updateOptimistic({ type: 'updateGroup', group, updates });
try {
const result = await updateItemGroup(group, updates);
// 成功:更新整组
setItems(prev =>
prev.map(item =>
item.group === group
? { ...item, ...updates }
: item
)
);
// 清除该组的错误
setGroupErrors(prev => {
const newErrors = { ...prev };
delete newErrors[group];
return newErrors;
});
} catch (error) {
// 失败:整组回滚
setGroupErrors(prev => ({
...prev,
[group]: error.message
}));
}
};
// 按组分类items
const groupedItems = optimisticItems.reduce((acc, item) => {
if (!acc[item.group]) {
acc[item.group] = [];
}
acc[item.group].push(item);
return acc;
}, {});
return (
<div>
{Object.entries(groupedItems).map(([group, items]) => (
<div key={group} className="group">
<h3>
{group}
{groupErrors[group] && (
<span className="error">
{groupErrors[group]}
</span>
)}
</h3>
<ul>
{items.map(item => (
<li
key={item.id}
className={item.pending ? 'pending' : ''}
>
{item.name}
</li>
))}
</ul>
<button
onClick={() => handleUpdateGroup(group, { status: 'completed' })}
>
完成整组
</button>
</div>
))}
</div>
);
}第五部分:用户体验优化
5.1 视觉反馈
jsx
'use client';
import { useOptimistic, useState } from 'react';
export default function VisualFeedback({ initialData }) {
const [data, setData] = useState(initialData);
const [operationState, setOperationState] = useState('idle');
const [optimisticData, setOptimisticData] = useOptimistic(
data,
(current, updates) => ({ ...current, ...updates })
);
const handleUpdate = async (updates) => {
setOperationState('optimistic');
setOptimisticData(updates);
try {
await new Promise(resolve => setTimeout(resolve, 500));
const result = await updateData(updates);
setOperationState('confirming');
await new Promise(resolve => setTimeout(resolve, 300));
setData(result);
setOperationState('success');
setTimeout(() => setOperationState('idle'), 1000);
} catch (error) {
setOperationState('error');
setTimeout(() => setOperationState('idle'), 2000);
}
};
return (
<div className={`data-container state-${operationState}`}>
<div className="value-display">
{optimisticData.value}
</div>
<div className="state-indicator">
{operationState === 'optimistic' && (
<span className="optimistic">⏳ 更新中...</span>
)}
{operationState === 'confirming' && (
<span className="confirming">✓ 确认中...</span>
)}
{operationState === 'success' && (
<span className="success">✓ 已保存</span>
)}
{operationState === 'error' && (
<span className="error">✗ 已回滚</span>
)}
</div>
<button onClick={() => handleUpdate({ value: optimisticData.value + 1 })}>
增加
</button>
</div>
);
}
/* CSS */
/*
.state-optimistic {
opacity: 0.7;
background-color: #fff3cd;
}
.state-confirming {
animation: pulse 0.5s;
}
.state-success {
background-color: #d4edda;
}
.state-error {
animation: shake 0.5s;
background-color: #f8d7da;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
*/5.2 离线支持
jsx
'use client';
import { useOptimistic, useState, useEffect } from 'react';
export default function OfflineSupport({ initialData }) {
const [data, setData] = useState(initialData);
const [isOnline, setIsOnline] = useState(true);
const [pendingUpdates, setPendingUpdates] = useState([]);
const [optimisticData, setOptimisticData] = useOptimistic(
data,
(current, updates) => ({ ...current, ...updates })
);
// 监听网络状态
useEffect(() => {
const handleOnline = () => {
setIsOnline(true);
// 重新发送待处理的更新
syncPendingUpdates();
};
const handleOffline = () => {
setIsOnline(false);
};
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
const syncPendingUpdates = async () => {
for (const update of pendingUpdates) {
try {
const result = await updateData(update);
setData(result);
} catch (error) {
console.error('同步失败:', error);
}
}
setPendingUpdates([]);
};
const handleUpdate = async (updates) => {
// 始终乐观更新
setOptimisticData(updates);
if (!isOnline) {
// 离线时保存到待处理列表
setPendingUpdates(prev => [...prev, updates]);
toast.info('离线状态,更新将在联网后同步');
return;
}
try {
const result = await updateData(updates);
setData(result);
} catch (error) {
toast.error('更新失败');
}
};
return (
<div>
{!isOnline && (
<div className="offline-banner">
您当前处于离线状态,更新将在联网后自动同步
{pendingUpdates.length > 0 && (
<span>(待同步: {pendingUpdates.length})</span>
)}
</div>
)}
<input
value={optimisticData.value}
onChange={(e) => handleUpdate({ value: e.target.value })}
/>
</div>
);
}注意事项
1. 不要手动回滚
jsx
// ❌ 错误:手动回滚
const handleLike = async () => {
setOptimisticLikes(likes + 1);
try {
await likePost(postId);
} catch (error) {
// 错误!不需要手动回滚
setOptimisticLikes(likes);
}
};
// ✅ 正确:让useOptimistic自动回滚
const handleLike = async () => {
setOptimisticLikes(likes + 1);
try {
const newLikes = await likePost(postId);
setLikes(newLikes); // 只需更新成功状态
} catch (error) {
// 自动回滚,只需处理错误提示
alert('操作失败');
}
};2. 提供清晰的错误信息
jsx
// ✅ 清晰的错误消息
try {
await updateTodo(id);
} catch (error) {
if (error.status === 404) {
setError('待办事项不存在');
} else if (error.status === 403) {
setError('没有权限修改');
} else {
setError('更新失败,请重试');
}
}3. 考虑网络状况
jsx
// ✅ 检测网络状态
const handleLike = async () => {
if (!navigator.onLine) {
alert('网络连接已断开');
return;
}
setOptimisticLikes(likes + 1);
try {
const newLikes = await likePost(postId);
setLikes(newLikes);
} catch (error) {
alert('点赞失败');
}
};4. 避免回滚闪烁
jsx
// ✅ 添加最小延迟避免闪烁
const handleUpdate = async (updates) => {
setOptimisticData(updates);
try {
// 确保至少300ms的延迟,避免瞬间回滚
const [result] = await Promise.all([
updateData(updates),
new Promise(resolve => setTimeout(resolve, 300))
]);
setData(result);
} catch (error) {
// 回滚
}
};5. 保持状态一致性
jsx
// ✅ 确保状态同步
const handleUpdate = async (updates) => {
setOptimisticData(updates);
try {
const result = await updateData(updates);
// 使用服务器返回的数据,而不是本地数据
setData(result);
} catch (error) {
// 自动回滚
}
};常见问题
Q1: useOptimistic如何知道何时回滚?
A: 当异步操作完成后,如果没有更新实际状态(likes),useOptimistic会自动回滚到实际状态的值。
Q2: 可以阻止自动回滚吗?
A: 不能也不应该。自动回滚是useOptimistic的核心特性,确保UI始终反映真实状态。
Q3: 如何处理多个并发的乐观更新?
A: 每个useOptimistic独立管理,互不干扰。多个更新可以同时进行,各自处理成功或失败。
Q4: 回滚会触发重新渲染吗?
A: 会。回滚时optimistic值变化,组件会重新渲染以显示正确的状态。
Q5: 如何避免频繁回滚?
A:
- 在发送请求前进行客户端验证
- 改善网络连接质量
- 实现合理的重试机制
- 使用防抖/节流减少请求频率
Q6: 回滚时如何保留用户输入?
A: 使用受控组件并维护独立的输入状态:
jsx
const [inputValue, setInputValue] = useState('');
const [savedValue, setSavedValue] = useState('');
const [optimisticValue, setOptimisticValue] = useOptimistic(
savedValue,
(_, newValue) => newValue
);
const handleSave = async () => {
setOptimisticValue(inputValue);
try {
await saveValue(inputValue);
setSavedValue(inputValue);
} catch (error) {
// 回滚,但inputValue保持不变
}
};总结
错误回滚要点
✅ useOptimistic自动回滚
✅ 无需手动处理回滚
✅ 提供清晰错误消息
✅ 实现重试机制
✅ 处理部分失败
✅ 显示待确认状态
✅ 考虑网络状况
✅ 优化视觉反馈
✅ 支持离线操作最佳实践
1. 信任自动回滚机制
2. 只处理错误提示和用户反馈
3. 实现合理的重试策略
4. 提供手动重试选项
5. 显示清晰的操作状态
6. 处理批量操作和部分失败
7. 提供优雅的错误UI
8. 考虑离线场景
9. 添加操作日志和监控
10. 测试各种失败场景用户体验建议
✅ 使用动画平滑过渡
✅ 提供即时的视觉反馈
✅ 显示操作进度
✅ 清晰的成功/失败提示
✅ 合理的重试选项
✅ 离线状态提示
✅ 保留用户输入
✅ 避免闪烁和跳动测试清单
□ 测试网络失败场景
□ 测试服务器错误响应
□ 测试并发更新
□ 测试部分成功场景
□ 测试离线/在线切换
□ 测试重试机制
□ 测试回滚动画
□ 测试错误消息显示
□ 测试状态一致性
□ 测试性能影响完善的错误回滚机制让乐观更新既快速又可靠!