TanStack / query

🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.
https://tanstack.com/query
MIT License
40.09k stars 2.69k forks source link

Changing `retryDelay` is ignored for query #7295

Closed burnedikt closed 3 weeks ago

burnedikt commented 4 weeks ago

Describe the bug

tl;dr; When retryDelay is updated (e.g. based on a react state variable) the useQuery hook does not seem to update the underlying retryer.


I've tried to build a query which polls an endpoint until it succeeds. Per default, it's okay to have quite a forgiving and long retry delay but based on some user action, I can actually assume the query might succeed sooner and I wanted to adjust the retryDelay (basically using it as polling interval in this case) accordingly. I then introduced a state variable which I update over the course of time and was expecting the query retry logic to adapt to it - but it didn't.

Your minimal, reproducible example

https://stackblitz.com/edit/vitejs-vite-u6rzza?file=src%2FApp.tsx

Steps to reproduce

  1. introduce state variable to control retryDelay
  2. use the state variable to drive the retryDelay option of useQuery
  3. Observe that changes to retryDelay don't take effect

Expected behavior

As a user, I expected that changes to the query configuration (like changing the retryDelay) will have an effect and change the query's behaviour but instead (and contrary to other query options like e.g. refetchInterval) a change in the retryDelay did not change the retry behaviour of the query.

How often does this bug happen?

Every time

Screenshots or Videos

No response

Platform

Tanstack Query adapter

react-query

TanStack Query version

v5.29.2

TypeScript version

v5.2.2

Additional context

I am aware there are workarounds out there like using refetchInterval instead (this is the same example from the reproducible stackblitz using refetchInterval instead) but this requires considering the queryFn not failed so that the refetch can happen and then check based on the (fake) data whether or not to keep refetching.

Another workaround I found is to include the retryDelay within the queryKey but while this gets the job done, it feels very awkward.

I don't consider this to be a big issue and probably an edge case but it still looks like unexpected behaviour to me and it cost me a couple of hours figuring out what's going on, so I thought I'd share it.

peVelosa commented 3 weeks ago

I had the same issue trying to update a variable inside useQuerywhitout updating the queryKey

useQuery will only update the variables within it if the queryKey is changed. As it stands, even changing the retryDelay the query will not have its values ​​updated, since the queryKey has not changed If you change line 12 to this one you will see that the problem has been solved

    queryKey: ['test', retryDelay],

refetchInterval is used to revalidate the query, so it will only work if it is successful.

I think that it happens because of the caching in react-query. The function will not update is own values if the key doesn't change, it will only update the data.

Maybe I'm making a mistake trying to prove you my point (I've heard it while a go, so that is the reason that I know it), but maybe the useQuery component will only trigger the useEffect and rerender it state when some of the options in queryKey, queryFn changes.

image

https://github.com/TanStack/query/blob/main/packages/react-query/src/useBaseQuery.ts#L91

TkDodo commented 3 weeks ago

the options do update on the query, but not on the retry. Let me explain:

when a query starts to run, we create an internal retryer and pass options from the query to it:

https://github.com/TanStack/query/blob/7368bd0f62360c2395eb31b1abf97dbaf0af46ce/packages/query-core/src/query.ts#L527-L529

Then, that retryer takes those options, returns a promise and will resolve or reject it once the whole process of fetching is complete. This takes retries, network availability etc. into account. Fore example, the query might go into paused state in between and continue with retries later.

So the retryer will see the options from the time it was created (=when the fetch started). Usually, this is what you want because the retry process is opaque. The promise will resolve or reject eventually, and then, when a refetch occurs (which is different from a retry), the new values will be respected.

We could make those values reactive in between retries, but I think that'll open up a can of worms. What if you change retries from 5 to 2 while you are at the 3rd retry? What if you change networkMode to 'always' while the query is paused because you have no network connection? It's the safer approach to just trigger the whole process (fetch + retries) with a given set of options from creation time until it is finished. It's a bit how closures work with react functions, too.

Both retry and retryDelay can be a function, so you can define them as a function and use a ref to read the latest value inside of it if you really want.