Tanstack Query:带回滚的多链式POST请求实现问题
Tanstack Query 链式POST请求与回滚实现方案
核心实现思路
- 给每个业务POST请求配套实现回滚接口(比如创建资源后回滚就删除该资源,提交数据后回滚就重置对应数据),并封装成Tanstack Query的mutation。
- 用一个总mutation串联所有请求流程,把链式调用、失败回滚逻辑都放在这个总mutation的执行函数里统一处理,避免多个独立mutation的状态混乱。
- 用分层try-catch包裹每一步请求:成功后保存关键数据(比如创建的资源ID,用于回滚),失败时立即触发已成功步骤的回滚操作,同时做好回滚失败的日志记录。
代码示例
1. Service层封装(service.tsx)
import { useMutation } from '@tanstack/react-query'; // 请求1:创建资源A const createResourceA = async (data: ResourceAData) => { const res = await fetch('/api/resource-a', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!res.ok) throw new Error('创建资源A失败'); return res.json(); }; // 回滚请求1:删除资源A const rollbackResourceA = async (resourceAId: string) => { const res = await fetch(`/api/resource-a/${resourceAId}`, { method: 'DELETE', }); if (!res.ok) throw new Error('回滚资源A失败'); }; // 请求2:创建资源B const createResourceB = async (data: ResourceBData) => { const res = await fetch('/api/resource-b', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!res.ok) throw new Error('创建资源B失败'); return res.json(); }; // 回滚请求2:删除资源B const rollbackResourceB = async (resourceBId: string) => { const res = await fetch(`/api/resource-b/${resourceBId}`, { method: 'DELETE', }); if (!res.ok) throw new Error('回滚资源B失败'); }; // 请求3:提交最终数据 const submitFinalData = async (data: FinalData) => { const res = await fetch('/api/final-submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!res.ok) throw new Error('提交最终数据失败'); return res.json(); }; // 导出单个请求的mutation(可选,也可直接在总mutation里调用函数) export const useCreateResourceAMutation = () => useMutation({ mutationFn: createResourceA, }); export const useRollbackResourceAMutation = () => useMutation({ mutationFn: rollbackResourceA, }); // 同理导出其他mutation...
2. 总mutation与组件使用
import { useMutation } from '@tanstack/react-query'; import { createResourceA, rollbackResourceA, createResourceB, rollbackResourceB, submitFinalData } from './service'; const useChainPostMutation = () => { return useMutation({ mutationFn: async (payload: { resourceAData: ResourceAData; resourceBData: ResourceBData; finalData: FinalData }) => { const { resourceAData, resourceBData, finalData } = payload; let resourceAId: string | undefined; let resourceBId: string | undefined; try { // 步骤1:发起第一个请求 const resourceA = await createResourceA(resourceAData); resourceAId = resourceA.id; try { // 步骤2:请求1成功,发起第二个请求 const resourceB = await createResourceB(resourceBData); resourceBId = resourceB.id; try { // 步骤3:请求2成功,发起第三个请求 await submitFinalData(finalData); return { success: true, message: '所有请求执行成功' }; } catch (submitError) { // 步骤3失败,回滚请求1和2 console.error('提交失败,开始回滚:', submitError); try { if (resourceBId) await rollbackResourceB(resourceBId); if (resourceAId) await rollbackResourceA(resourceAId); } catch (rollbackError) { console.error('回滚资源A/B失败:', rollbackError); // 这里可以加告警逻辑,比如通知后端或记录日志 } throw new Error('提交最终数据失败,已尝试回滚资源A和B'); } } catch (resourceBError) { // 步骤2失败,回滚请求1 console.error('创建资源B失败,回滚资源A:', resourceBError); try { if (resourceAId) await rollbackResourceA(resourceAId); } catch (rollbackError) { console.error('回滚资源A失败:', rollbackError); } throw new Error('创建资源B失败,已尝试回滚资源A'); } } catch (resourceAError) { // 步骤1失败,直接终止 throw new Error('创建资源A失败,操作终止'); } }, onSuccess: (data) => { console.log(data.message); // 这里加成功后的UI提示,比如弹窗 }, onError: (error) => { console.error(error.message); // 这里加错误提示,比如Toast }, }); }; // 组件中使用 const MyComponent = () => { const chainMutation = useChainPostMutation(); const handleSubmit = async () => { try { await chainMutation.mutateAsync({ resourceAData: { /* 资源A的请求数据 */ }, resourceBData: { /* 资源B的请求数据 */ }, finalData: { /* 最终提交的数据 */ }, }); } catch (err) { // 处理错误,比如显示错误信息 } }; return ( <button onClick={handleSubmit} disabled={chainMutation.isLoading}> {chainMutation.isLoading ? '处理中...' : '执行链式请求'} </button> ); }; export default MyComponent;
关键注意事项
- 回滚接口幂等性:回滚操作要保证多次调用不会出错(比如重复删除同一个资源不报错),避免回滚失败导致数据不一致。
- 错误日志记录:回滚失败时一定要记详细日志,方便后续排查数据问题。
- 状态统一管理:总mutation的
isLoading状态可以统一控制整个流程的加载状态,不用单独处理多个mutation的加载状态。 - 优先后端事务:如果后端支持数据库事务,优先让后端把三个请求放在一个事务里处理,失败自动回滚,前端只发一个请求即可,这样更可靠。
内容的提问来源于stack exchange,提问作者Jeff Barnes
相关产品推荐
相关产品推荐

