vue-query
vue-query copied to clipboard
Query being run despite enabled being false
I have this seemingly very simple query that I would expect to never run if id is 0 or undefined, and most of the time it works. However, every now and then api.projects.get() gets called with 0, meaning the query is being run despited enabled seemingly being false. Have I messed up reactivity unwittingly, or is there some race condition I'm not aware of?
export const useProject = (id: MaybeRef<number | undefined>) =>
useQuery(
["projects", id],
() => api.projects.get(unref(id) ?? 0),
{ enabled: computed(() => !!unref(id)) },
);
Interesting.
Could you see what happens with the following code?
This will make sure that you will get the same queryKey value as when it was in enabled state.
export const useProject = (id: MaybeRef<number | undefined>) =>
useQuery(
["projects", id],
({ queryKey }) => api.projects.get(queryKey[1]),
{ enabled: computed(() => !!unref(id)) },
);
Also you could try to observe the state of the queries in devtools. To see if some unwanted queries are enabled, or created in the cache.
I will try that, although it is very hard to reproduce. I haven't been able to reproduce it locally at all, but it happens quite a lot in production, which seems to indicate it's some race condition. In devtools everything always looks OK. The queries are disabled as they should be.
I've done some more experimentation, and it seems like the queries are fired again when I call invalidateQueries, even if enabled is false. I've been able to reproduce that locally as well. I've had to resort to something like this:
export const useProject = (id: MaybeRef<number | undefined>) =>
useQuery(
keys.byId(id),
() => unref(id)
? api.projects.get(unref(id)!)
: null,
{ enabled: computed(() => !!unref(id)) },
);
I can't count on the fetch method not being called if enabled is false, and I can't return undefined because useQuery doesn't allow an undefined return. I browsed the TanStack query issues, and I saw that there were some issues related to this, but they were supposedly fixed. I'm on the latest version on vue-query, so I would think I would have the fixes.
Unfortunately using the queryKey doesn't work, since the query is fired when it shouldn't be.
Well, the logic that determines whether to refetch the query or not via invalidate is implemented in the query-core and is shared.
Therefore if it works properly in react-query it should work properly with vue-query as well.
I tested that in the basic example and it works as intended.
What i could suggest is to double check if all observers of said query are disabled. Cause if some are not, fetch will happen and it's correct.
One way would be to add a check in the code via queryCache.find/queryCache.findAll -> iterate over queries to find if all observers are disabled just before you send invalidate
Another one would be to inspect the devtools of said query

I did some more experimentation, and I did get some more information. I tried this:
export const useProject = (id: MaybeRef<number | undefined>) =>
useQuery(
keys.byId(id),
ctx => {
console.log(ctx);
console.log(id);
return unref(id)
? api.projects.get(unref(id)!)
: undefined;
},
{ enabled: computed(() => !!unref(id)) },
);
And I got this:

So yes as you suspected, the query key used (and I assume what is being used to determine the enabled state) is not in sync with what the current values of the query key components actually are. So yes, I could pass in the queryKey, and pick the values out of it, but there's a couple of problems with that. For one, it makes the code a lot more unclear. But worst of all, there's a reason the value is now undefined, it's because I don't want the query to be fired. If I pass in queryKey and use stale values, I will get the wrong result or an error from the server.
Also, to clarify, I don't have any other observers with a different enabled state, since I wrap every single usage of useQuery and the enabled state is directly linked to the values that make up the query key.
I guess my question now is how could the query key be out of sync with the current values? Is it read too far ahead of where the query is fired? Could it be the async/await state machine causing the update to the underlying values in-between when react-query gets queryKey and enabled are determined and when the query is re-run after being invalidated? I am doing an await on queryClient.invalidateQueries, which I noticed the react-query documentation didn't do, but I also tried searching for any warnings not to do that and I didn't find any.
My suspicion is that some async/await somewhere between where react-query is determining queryKey and enabled is changing the underlying query key components (in my code). I'm not sure if I'm doing something wrong with async/await, or if this is a very vue-specific reactivity issue that react-query doesn't have to contend with.
I'd definitely appreciate any insights.
What you are seeing in console is correct, cause id contains a ref and you should console.log(id.value) to get the correct comparison.
Also, to clarify, I don't have any other observers with a different enabled state, since I wrap every single usage of useQuery and the enabled state is directly linked to the values that make up the query key.
This does not mean that you do not have multiple observers. You can see it in the devtools. Like in the screenshot i have shared there is [1] next to stale label which indicates how many observers are subscribed to the query.
Thanks for the quick reply. The value is there in the screenshot as undefined, and if I log queryKey and unref(id) instead to get the values directly I get the same thing:

queryKey has stale values, id is undefined which is correct.
And yes I do have multiple observers, but what I meant is that every observer has the query key directly tied to the parameter passed into the wrapper. There will never be a case where enabled will be true for one observer and false for another for the same id. For any id that is 0 or undefined, enabled will always be false for all observers.
🤔
One more experiment i could think of is to add await flush promises() before invalidation and I'd/query key check.
flushPromises is basically a promise that resolves after 0 timeout. Ex. new Promise((resolve) => {setTimeout(resolve,0)})