事情的起因是这样的,在使用react开发项目的时候,邮箱规则列表页面有一个Switch按钮,点击按钮可以切换规则的启用或者禁用状态,刚开始的做法是:用户点击了按钮之后先改变IsEnabled的状态,然后调用update接口,更新服务器端的接口,使用await等到update接口调用成功之后,再去调用获取邮箱规则列表的接口,更新列表状态:
const handleEdit = async (flag: boolean, item: MailRuleItem) => {
// 1. 创建更新后的对象(使用函数式更新确保最新状态)
const updateItem = {
...item,
IsEnabled: flag ? 1 : 0,
Actions: {
...item.Actions,
MoveToFolder: item.Actions.MoveToFolder?.UniqueId,
Delete: item.Actions.Delete ? '1' : '0',
MarkAsRead: item.Actions.MarkAsRead ? '1' : '0',
CopyToFolder: item.Actions.CopyToFolder?.UniqueId,
},
Conditions: { ...item.Conditions }
};
await updatemailrule(updateItem);//更新的接口
onReload();//刷新列表
};
遇到的问题是:update接口响应比较慢,导致点击完按钮之后没有反应,好像是按钮没被点击一样,用户体验不好
需求:点击完之后,立马更新按钮的状态,如果接口发生错误,需要恢复按钮的状态
这里通过各种搜索,了解到了乐观更新,这个词听着很陌生,但其实很简单,原理就是点击完按钮之后,立马把被点击的数据的状态更改,给人的感觉是接口已经调用成功了,但实际仅仅是前端意义的更改,关于乐观更新解释如下:
乐观更新是一种提升用户体验的前端优化策略,其核心思想是:在等待服务器响应前,先假设操作会成功,立即更新本地UI状态。如果最终请求失败,再回滚到之前的状态。
核心特点
-
即时反馈
- 用户操作后UI立即变化,无需等待网络延迟
- 典型场景:点赞、开关切换、列表项操作
-
异步补偿
- 请求失败时自动回滚状态
- 通常会配合错误提示
-
状态可逆
- 必须保留操作前的状态副本
- 回滚时需要准确恢复上下文
实现原理(三步流程)
用户->>UI: 触发操作(如点击开关)
UI->>UI: 立即更新本地状态(乐观更新)
UI->>服务端: 发送异步请求
alt 请求成功
服务端-->>UI: 返回200
UI->>UI: 保持更新后状态(可选二次确认)
else 请求失败
服务端-->>UI: 返回错误
UI->>UI: 自动回滚状态+错误提示
end
典型实现代码(React示例)
const [items, setItems] = useState(data);
const handleToggle = async (id) => {
// 1. 保存原始状态
const originalItems = [...items];
// 2. 乐观更新:立即切换UI状态
setItems(prev => prev.map(item =>
item.id === id ? { ...item, active: !item.active } : item
));
try {
// 3. 发送真实请求
await api.toggleItem(id);
} catch (error) {
// 4. 失败时回滚
setItems(originalItems);
toast.error("更新失败");
}
};
适用场景 vs 不适用场景
| 适合场景 | 不适合场景 |
|---|---|
| ✅ 高频交互操作(点赞/收藏) | ❌ 金融交易等关键操作 |
| ✅ 延迟敏感型功能(开关切换) | ❌ 依赖服务端复杂计算的场景 |
| ✅ 幂等性操作(可重复执行) | ❌ 非幂等性操作 |
最后使用乐观更新问题得以解决:
const handleEdit = async (flag: boolean, item: MailRuleItem) => {
// 1. 创建更新后的对象(使用函数式更新确保最新状态)
const updateItem = {
...item,
IsEnabled: flag ? 1 : 0,
Actions: {
...item.Actions,
MoveToFolder: item.Actions.MoveToFolder?.UniqueId,
Delete: item.Actions.Delete ? '1' : '0',
MarkAsRead: item.Actions.MarkAsRead ? '1' : '0',
CopyToFolder: item.Actions.CopyToFolder?.UniqueId,
},
Conditions: { ...item.Conditions }
};
// 2. 乐观更新 - 使用函数式更新确保基于最新状态
setRules(prevRules => {
// 返回更新后的状态
return prevRules.map(rule => {
if (rule.Id === item.Id) {
return updateItem;
} else {
return rule;
}
}
);
});
try {
// 3. 发送异步请求
await updatemailrule(updateItem);
// 4. 请求成功后,可以调用onReload获取最新数据
// 或者直接使用返回的更新后数据(更推荐)
onReload();
} catch (error) {
// 5. 请求失败时,回滚到之前保存的快照
setRules(prevRules => {
// 找到当前项的原始状态
const originalItem = prevRules.find(r => r.id === item.id);
if (!originalItem) return prevRules;
return prevRules.map(rule => {
if (rule.id === item.id) {
return originalItem;
} else {
return rule;
}
}
);
});
console.error("更新失败:", error);
}
};