Stop Refetching Everything
My devs experience on boosting UI performance issues.
TL;DR
One small mutation can triggered unnecessary API requests in React and Tanstack/React-Query
The Discovery
The UI looked correct, but one mutation produced multiple requests:
Event:
PUT /graphs/task-id
Mutation Request:
GET /graphs/list-idGET /graphs/workspace-id/subgraphGET /graphs/workspace-id?relation=has_status_templateGET /graphs/workspace-id?relation=has_tagsGET /graphs/workspace-id?relation=has_priorities
Create, update, and delete all showed the same pattern. Every mutation called the same invalidation function:
const invalidateGraphQueries = () => {
queryClient.invalidateQueries({ queryKey: ["graphNode"] });
queryClient.invalidateQueries({ queryKey: ["workspace-hierarchy"] });
queryClient.invalidateQueries({ queryKey: ["workspace-statuses"] });
queryClient.invalidateQueries({ queryKey: ["workspace-tags"] });
queryClient.invalidateQueries({ queryKey: ["workspace-priorities"]});
};
It was safe because the UI always received fresh data, but it was also wasteful. Updating one task did not change the hierarchy, tags, or priorities.
Why It Happened
React Query uses prefix matching:
queryClient.invalidateQueries({
queryKey: ["graphNode"],
});
This can match all queries beginning with graphNode:
["graphNode", "task-list"]
["graphNode", "task-id"]
["graphNode", "document-id"]
Our requests also used a timestamp:
graphService.getGraph(id, {
_t: Date.now(),
});
Because the URL changed every time, each invalidation resulted in a real network request.
Solution
Adding each mutation to declare what it changed:
type GraphInvalidateScope =
| "nodes"
| "hierarchy"
| "statuses"
| "tags"
| "priorities"
| "taskLogs";
A task update now invalidates only node queries:
updateGraph(taskId, data, message, {
workspaceId,
scope: ["nodes"],
});
A tag update invalidates only tags:
updateGraph(tagId, data, message, {
workspaceId,
scope: ["tags"],
});
The shared helper handles those scopes:
const invalidateByScope = (
scope: GraphInvalidateScope[],
workspaceId?: string,
) => {
if (scope.includes("nodes")) {
queryClient.invalidateQueries({
queryKey: ["graphNode"],
});
}
if (scope.includes("tags")) {
queryClient.invalidateQueries({
queryKey: ["workspace-tags", workspaceId],
});
}
if (scope.includes("statuses")) {
queryClient.invalidateQueries({
queryKey: ["workspace-statuses", workspaceId],
});
}
};
Result
| Metric | Before | After |
|---|---|---|
| Refetches triggered per mutation | 5–7 | 1 |
| Invalidation strategy | Broad | Targeted |
| Mutation context | Limited | Sufficient to identify affected data |
| Network overhead | High | Lower |