在React中实现乐观更新(Optimistic Updates)时,如果接口调用失败,需要通过以下策略进行回滚:
🌙 一、基础乐观更新模式(带回滚)
import { useState, useOptimistic } from 'react';
function TodoList({ todos }: { todos: Todo[] }) {
const [optimisticTodos, setOptimisticTodos] = useOptimistic(
todos,
(state, newTodo: Todo) => [...state, newTodo]
);
const [error, setError] = useState<Error | null>(null);
async function addTodo(newTodo: Todo) {
// 1. 保存当前状态到ref(不会触发渲染)
let rollbackData = todos;
// 2. 乐观更新UI
setOptimisticTodos(newTodo);
try {
// 3. 实际API调用
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo)
});
} catch (err) {
// 5. 失败时回滚
setOptimisticTodos(rollbackData);
setError(err as Error);
}
}
return (
<>
{error && <ErrorToast error={error} onDismiss={() => setError(null)} />}
{/* 使用optimisticTodos渲染列表 */}
</>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
🌙 二、增强版带重试机制的乐观更新
import { useOptimistic, useTransition } from 'react';
function useOptimisticUpdate<T>(initialData: T) {
const [isPending, startTransition] = useTransition();
const [data, setData] = useOptimistic(initialData);
const [retryQueue, setRetryQueue] = useState<(() => Promise<void>)[]>([]);
async function executeWithRetry(
action: () => Promise<void>,
optimisticUpdate: (current: T) => T
) {
let rollbackData = data;
let retryCount = 0;
const maxRetries = 3;
const execute = async () => {
try {
// 乐观更新
startTransition(() => {
setData(optimisticUpdate);
});
await action();
} catch (error) {
if (retryCount < maxRetries) {
retryCount++;
await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
throw error; // 触发重试
} else {
// 最终失败时回滚
startTransition(() => {
setData(rollbackData);
});
throw error;
}
}
};
try {
await execute();
} catch (error) {
// 加入重试队列
setRetryQueue(prev => [...prev, execute]);
}
}
// 暴露给组件的API
return {
data,
isPending,
executeWithRetry,
retryAll: () => Promise.all(retryQueue.map(fn => fn()))
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
🌙 三、与SWR/React Query配合的方案
import { useSWRConfig } from 'swr';
import useOptimistic from 'swr/optimistic';
function useOptimisticMutation() {
const { mutate } = useSWRConfig();
const [error, setError] = useState<Error | null>(null);
async function mutateWithRollback(
key: string,
asyncUpdate: () => Promise<any>,
optimisticData: any
) {
// 1. 乐观更新
mutate(key, optimisticData, false);
try {
// 2. 执行实际请求
const result = await asyncUpdate();
// 3. 用实际数据重新验证
mutate(key, result);
return result;
} catch (err) {
// 4. 回滚到之前状态
mutate(key);
setError(err as Error);
throw err;
}
}
return { mutateWithRollback, error };
}
// 在组件中使用
function EditPost({ postId }) {
const { data } = useSWR(`/api/posts/${postId}`);
const { mutateWithRollback } = useOptimisticMutation();
const handleSave = async (newData) => {
await mutateWithRollback(
`/api/posts/${postId}`,
() => fetch(`/api/posts/${postId}`, {
method: 'PUT',
body: JSON.stringify(newData)
}),
newData // 乐观更新数据
);
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
🌙 四、完整事务型乐观更新Hook
import { useCallback, useState } from 'react';
type OptimisticUpdate<T> = {
data: T;
error: Error | null;
execute: (
optimisticValue: T,
asyncAction: () => Promise<void>,
onRollback?: (error: Error) => void
) => Promise<void>;
reset: () => void;
};
export function useOptimisticUpdate<T>(initialData: T): OptimisticUpdate<T> {
const [data, setData] = useState<T>(initialData);
const [error, setError] = useState<Error | null>(null);
const [history, setHistory] = useState<{ data: T; timestamp: number }[]>([]);
const execute = useCallback(
async (
optimisticValue: T,
asyncAction: () => Promise<void>,
onRollback?: (error: Error) => void
) => {
// 保存当前状态到历史记录
setHistory(prev => [...prev, { data, timestamp: Date.now() }]);
// 乐观更新
setData(optimisticValue);
setError(null);
try {
await asyncAction();
// 成功时清除历史记录
setHistory([]);
} catch (err) {
// 失败时回滚
const error = err as Error;
setError(error);
if (history.length > 0) {
const lastState = history[history.length - 1];
setData(lastState.data);
} else {
setData(initialData);
}
onRollback?.(error);
}
},
[data, history, initialData]
);
const reset = useCallback(() => {
setData(initialData);
setError(null);
setHistory([]);
}, [initialData]);
return { data, error, execute, reset };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
🌙 五、生产环境最佳实践
- 错误恢复UI模式
function SaveButton() {
const { data, error, execute } = useOptimisticUpdate(initialData);
const handleClick = async () => {
await execute(
newData,
async () => {
const response = await fetch('/api/data', {
method: 'POST',
body: JSON.stringify(newData)
});
if (!response.ok) throw new Error('Save failed');
},
(error) => {
// 自定义回滚逻辑
analytics.track('rollback_triggered', { error: error.message });
}
);
};
return (
<>
<button onClick={handleClick} disabled={!!error}>
{error ? 'Retry' : 'Save'}
</button>
{error && (
<div className="error-banner">
Save failed: {error.message}
<button onClick={() => execute(...)}>Retry</button>
<button onClick={reset}>Discard changes</button>
</div>
)}
</>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
- 性能优化建议
// 使用useTransition避免UI阻塞
const [isPending, startTransition] = useTransition();
const execute = async () => {
startTransition(() => {
setData(optimisticValue);
});
await asyncAction();
};
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
- 与后端协作规范
// 理想的后端响应格式
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: {
code: string;
message: string;
recoverable: boolean; // 是否可重试
};
metadata?: {
version?: string; // 用于乐观并发控制
timestamp?: number;
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
🌙 六、不同方案的适用场景
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
useOptimistic Hook | React原生支持 | 需要18+版本 | 简单乐观更新 |
SWR/React Query集成 | 完善的缓存策略 | 需要学习额外库 | 数据获取场景 |
自定义事务Hook | 完全控制流程 | 实现复杂度高 | 关键业务操作 |
Redux中间件方案 | 状态集中管理 | 冗余代码多 | 大型应用 |
🌙 总结
在React中实现带回滚的乐观更新时:
- 必须保留原始数据用于回滚
- 错误处理要区分可恢复/不可恢复错误
- 提供清晰的重试机制给用户
- 与后端约定错误格式便于恢复
- 复杂场景考虑使用状态管理库
对于关键业务操作(如支付、订单修改),建议使用自定义事务Hook或Redux中间件方案;对于普通数据更新,使用useOptimistic
或SWR集成就足够。