乐观 UI 更新
立即显示预期结果并在服务器响应时对账。
UX 模式进阶
为什么
等 300ms 点赞切换的服务器往返感迟。乐观 UI 即时应用变更,错误时回滚 — 感知性能改善即使网络慢。
Nielsen Norman Group 研究显示用户在离开前平均停留 10-15 秒。清晰的视觉层次和渐进式披露将任务完成率提高 40-60%。
何时使用
用于低风险、易可逆动作:点赞、收藏、已读/未读、勾选。避免不可逆动作(删除、支付)或陈旧数据致害(余额、库存)。
如何做
1. 本地应用 — 请求前即时变更缓存/UI 到预期状态
2. 发请求 — 后台触发 POST/PUT
3. 成功 — 对账:服务器可能返回规范状态(如新点赞数),替换本地
4. 错误 — 回滚到前状态,显 toast("无法点赞 — 重试")
5. 冲突 — 服务器说状态已变则重取并显真相
6. 库 — React Query 的 onMutate + onError 回滚、SWR 的 optimisticData、或手卷
7. 幂等 — 使动作幂等(点赞切换非点赞递增)以让重试安全
示例
// React Query — 乐观点赞切换
useMutation({
mutationFn: (liked: boolean) => api.put(`/posts/${id}/like`, { liked }),
onMutate: async (liked) => {
await queryClient.cancelQueries({ queryKey: ["post", id] });
const prev = queryClient.getQueryData(["post", id]);
queryClient.setQueryData(["post", id], (p) => ({ ...p, liked })); // 乐观
return { prev }; // 回滚上下文
},
onError: (_e, _v, ctx) => {
queryClient.setQueryData(["post", id], ctx?.prev); // 回滚
toast.error("无法更新 — 已回滚");
},
onSettled: () => queryClient.invalidateQueries({ queryKey: ["post", id] }),
});
// 手卷 — 同模式
const [liked, setLiked] = useState(post.liked);
const toggle = async () => {
const prev = liked;
setLiked(!prev); // 乐观
try { await api.put(`/posts/${id}/like`, { liked: !prev }); }
catch { setLiked(prev); toast.error("已回滚"); } // 回滚
};
常见问题
- ⚠错误无回滚 — UI 永显乐观状态,与服务器发散
- ⚠用于不可逆动作 — 用户乐观 '删除',服务器失败,UI 数据无但服务器有
- ⚠非幂等动作重试 — 重试双点赞,服务器数错
最佳实践
- ✓始终提供 onError 回滚 — 请求失败时乐观状态须回退
- ✓仅用于可逆动作 — 点赞、收藏、切换;绝不乐观删除或支付
- ✓使动作幂等 — 点赞切换(非递增)以让重试不双计