I'm using the T3 Stack with tRPC and facing a challenge with query invalidation across different routes. My application has two routes: /profile
and /student-profile
.
On /profile
: I display a list with data about specific tasks ("TODO" items).On /student-profile
: Users can accept or reject a student for a task, using two buttons.After performing a mutation (accepting or rejecting the student) on /student-profile
, I need to invalidate the task list data on /profile
to update it accordingly. However, even though the mutation is successful, the invalidation does not trigger, as confirmed by my logs.
I realized that the "list" query becomes inactive when navigating to /student-profile
, preventing the invalidation.
Here's my constraint: I want to avoid setting refetchOnMount: true
or cacheTime: 0
for this query. The reason is to prevent making an additional network request every time the /profile
page is visited. This is crucial for reducing unnecessary network traffic and improving user experience.
My question is: How can I effectively invalidate an inactive query (the list on /profile
) from another route (/student-profile`) in this setup, while adhering to my constraint of not frequently refetching the data? Is there a specific approach in tRPC within the T3 Stack to handle such a scenario?
Any guidance or insights on this issue would be very helpful.
Thank you in advance!
/profile
route
const {data: tasks, isFetching} = api.tasks.getInterestedTasksByAuthorId.useQuery({
authorId: user.id
}, {
refetchOnMount: false,
refetchOnWindowFocus: false,
});
/student-profile
route
const acceptStudent = api.tasks.acceptStudent.useMutation({
async onSuccess() {
try {
await utils.tasks.getInterestedTasksByAuthorId.invalidate()
} catch (e) {
console.error(e)
}
},
});
const handleAcceptStudent = async () => {
await acceptStudent.mutateAsync({
studentId,
taskId
})
}
How can I effectively invalidate an inactive query
There are a couple of ways to do this directly in your mutation callback:
filters
, where you can specify a refetchType
that defaults to 'active'
. Set it to 'all'
instead to refetch all matching queries, even the ones that aren't active:utils.tasks.getInterestedTasksByAuthorId.invalidate(undefined, { refetchType: 'all' })
Note that in trpc, the filters are the second param, so if you have no input, you need to pass undefined
as the first param.
.refetch
instead of .invalidate
, which pretty much does the same thing as invalidate with refetchType: 'all'
:utils.tasks.getInterestedTasksByAuthorId.refetch()
The problem with both approaches is that they just target everything they match. This might work for your case, but let's consider that your todo items can be filtered or searched. Because of the document cache that react-query has, you will have multiple cache entries (one per input) that will match. So if you have 3 potential todo-items lists in your cache, all being inactive, they will all refetch on the spot, even though the user might never go back to them.
That's why invalidation is usually preferred. It will only mark items as stale that are inactive, and will only refetch them just-in-time when the user requests them the next time.
The reason is to prevent making an additional network request every time the /profile page is visited.
The thing is: you don't need to tweak flags like refetchOnMount
for that. Setting a high staleTime
for your resource is the preferred approach. If you set staleTime
to 20 minutes, that means data will be considered fresh for 20 minutes, so every call to useQuery
will only read data from the cache in this timeframe, because the flags like refetchOnMount
and refetchOnWindowFocus
etc will only ever refetch data that is considered stale when the event comes up.
And that also plays very will with invalidation. Because even if you have staleTime
set to Infinity
(meaning: it will always be fresh), calling invalidateQueries()
essentially overwrites that.
So my preferred approach is to just leave all flags on their default settings, customize staleTime
to my liking and just use the default invalidate()
method, because it's the least amount of code to write and maintain, the best possible user experience and it will never unnecessarily overfetch.