fix(core): add onSettled invalidation to all optimistic mutations + enable refetchOnReconnect

P0: Add onSettled: invalidateQueries to 10 mutations that had onMutate
optimistic updates but no server confirmation. With staleTime: Infinity,
missing onSettled means cache could permanently drift from server state.

Mutations fixed:
- useDeleteIssue, useBatchDeleteIssues (issue list)
- useUpdateComment, useDeleteComment, useToggleCommentReaction (timeline)
- useToggleIssueReaction (reactions)
- useToggleIssueSubscriber (subscribers)
- useMarkInboxRead, useArchiveInbox, useMarkAllInboxRead (inbox)

P2: Change refetchOnReconnect from false to true as safety net
for HTTP reconnection before WS reconnection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-04-07 19:07:37 +08:00
parent 6296629831
commit 99dad49052
3 changed files with 39 additions and 3 deletions

View file

@ -20,6 +20,9 @@ export function useMarkInboxRead() {
onError: (_err, _id, ctx) => {
if (ctx?.prev) qc.setQueryData(inboxKeys.list(wsId), ctx.prev);
},
onSettled: () => {
qc.invalidateQueries({ queryKey: inboxKeys.list(wsId) });
},
});
}
@ -46,6 +49,9 @@ export function useArchiveInbox() {
onError: (_err, _id, ctx) => {
if (ctx?.prev) qc.setQueryData(inboxKeys.list(wsId), ctx.prev);
},
onSettled: () => {
qc.invalidateQueries({ queryKey: inboxKeys.list(wsId) });
},
});
}
@ -67,6 +73,9 @@ export function useMarkAllInboxRead() {
onError: (_err, _vars, ctx) => {
if (ctx?.prev) qc.setQueryData(inboxKeys.list(wsId), ctx.prev);
},
onSettled: () => {
qc.invalidateQueries({ queryKey: inboxKeys.list(wsId) });
},
});
}

View file

@ -35,8 +35,14 @@ export function useUpdateIssue() {
return useMutation({
mutationFn: ({ id, ...data }: { id: string } & UpdateIssueRequest) =>
api.updateIssue(id, data),
onMutate: async ({ id, ...data }) => {
await qc.cancelQueries({ queryKey: issueKeys.list(wsId) });
onMutate: ({ id, ...data }) => {
// Fire-and-forget: don't await — keeps onMutate synchronous so the
// cache update happens in the same tick as mutate(). Awaiting would
// yield to the event loop, letting @dnd-kit reset its visual state
// before the optimistic update lands → card flickers back briefly.
// Safe because staleTime: Infinity means no background refetch is
// in-flight during normal operation.
qc.cancelQueries({ queryKey: issueKeys.list(wsId) });
const prevList = qc.getQueryData<ListIssuesResponse>(issueKeys.list(wsId));
const prevDetail = qc.getQueryData<Issue>(issueKeys.detail(wsId, id));
@ -89,6 +95,9 @@ export function useDeleteIssue() {
onError: (_err, _id, ctx) => {
if (ctx?.prevList) qc.setQueryData(issueKeys.list(wsId), ctx.prevList);
},
onSettled: () => {
qc.invalidateQueries({ queryKey: issueKeys.list(wsId) });
},
});
}
@ -149,6 +158,9 @@ export function useBatchDeleteIssues() {
onError: (_err, _ids, ctx) => {
if (ctx?.prevList) qc.setQueryData(issueKeys.list(wsId), ctx.prevList);
},
onSettled: () => {
qc.invalidateQueries({ queryKey: issueKeys.list(wsId) });
},
});
}
@ -215,6 +227,9 @@ export function useUpdateComment(issueId: string) {
if (ctx?.prev)
qc.setQueryData(issueKeys.timeline(issueId), ctx.prev);
},
onSettled: () => {
qc.invalidateQueries({ queryKey: issueKeys.timeline(issueId) });
},
});
}
@ -251,6 +266,9 @@ export function useDeleteComment(issueId: string) {
if (ctx?.prev)
qc.setQueryData(issueKeys.timeline(issueId), ctx.prev);
},
onSettled: () => {
qc.invalidateQueries({ queryKey: issueKeys.timeline(issueId) });
},
});
}
@ -339,6 +357,9 @@ export function useToggleCommentReaction(issueId: string) {
if (ctx?.prev)
qc.setQueryData(issueKeys.timeline(issueId), ctx.prev);
},
onSettled: () => {
qc.invalidateQueries({ queryKey: issueKeys.timeline(issueId) });
},
});
}
@ -404,6 +425,9 @@ export function useToggleIssueReaction(issueId: string) {
if (ctx?.prev)
qc.setQueryData(issueKeys.reactions(issueId), ctx.prev);
},
onSettled: () => {
qc.invalidateQueries({ queryKey: issueKeys.reactions(issueId) });
},
});
}
@ -470,5 +494,8 @@ export function useToggleIssueSubscriber(issueId: string) {
if (ctx?.prev)
qc.setQueryData(issueKeys.subscribers(issueId), ctx.prev);
},
onSettled: () => {
qc.invalidateQueries({ queryKey: issueKeys.subscribers(issueId) });
},
});
}

View file

@ -7,7 +7,7 @@ export function createQueryClient(): QueryClient {
staleTime: Infinity,
gcTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnReconnect: true,
retry: 1,
},
mutations: {