-
-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
multiple updates outside of react transition when using suspense #3432
Comments
yes, strict effect might result in double request in dev mode only. react 18 is still not officially released, and react-query does not yet have support for it. To make our store work with 18, we need to switch to Feel free to try out the codesandbox preview build (by depending on:
The docs clearly state that:
|
@TkDodo thanks for the quick reply, and thoughtful response
Yes that's fair, however the term "experimental" can mean different things, and it goes on to say:
This is worded to suggest as long as I lock the versions, that things are "stable". As for your linked PR about
I suspect it's more than this, even without strict mode it happens. I just pushed another commit to my reproduction example removing strict mode, and the issue still persists.
The issue still happens, it should be fairly straightforward to clone my example and try whatever development versions you suspect may affect it, but I'm also happy to test it. What I don't get though is how we expect it to be "stable" with the mind bending issue of setting an arbitrary cache time. It seems to me that as long as it takes more than the hard coded delay in |
So I wanted to clarify I realized I misunderstood const variables = useMemo(() => {
return {
params: {
// some application specific code here
},
};
}, [validQueryParams]);
const myQueryResult = useGetMyQuery(variables, {
staleTime: Infinity,
suspense: true
});
const myData = useMemo(() => {
console.log('data changed,', JSON.stringify(myQueryResult.data));
return myQueryResult.data;
}, [JSON.stringify(myQueryResult.data)]);
useEffect(() => {
console.log('mount');
return () => console.log('unmount');
}, []); I basically want to be able to click a button that changes the variables, have react query run and update the data once. No matter what I do it triggers the updates like 6x, unless I try to memoize the If I turn suspense off, everything is fine but I lose the ability to show stale content during a transition. As soon as I turn on suspense the issue comes back, here is the order of the logs: Initial load:
Upon transition changing variables:
In fact Chrome devtools detects the JSON is unchanged and groups the logs together for me: I hope this clarifies the issue better, thank you! |
there is no need to
apart from logging, this doesn't do anything though.
That is what keepPreviousData: true is for.
you are mixing a lot of things in this issue, it's very hard for me to keep track. Is this now a suspense issue, or a memoization issue without suspense? A query runs as soon as it mounts. If you want to only run it when variables change, you need to initially disable it with the So please, let's start over, from zero. And please provide a codesandbox reproduction, that is so much easier to run for me than a repo. Thanks. |
While I generally agree, the point here is that my component is mounting twice. Once when it initially suspends, and again when the data resolves. I added the stringify just to be really sure. Although the string passed to useMemo is equal, the memo call fires more than once.
Thanks, that's helpful. Ideally we'd like to use React transitions to keep the stale data on screen, as there are numerous other benefits.
I don't know, to be honest. I've tried to make the example as minimal as possible, it's only a dozen or so lines of code. I created another example with just React, without
I expect it to run 1x not 2x (or really 6x)
I don't want to disable the query from running on mount. I just want it to run initially 1x, and 1x when the variables change. I never want
If that's the case, it's possible this is all due to an issue in React itself? I don't know. I expected it to be stable, but I'm confused on why my component is being updated 6x without memoization, and 2x with memoization. I expect it to only update 1x in both cases.
I guess this is the crux of my feedback. Thanks for letting me know about tracked queries, that's something I was not aware of. Given that I can replicate similar issues in React itself, I'd speculate it's not going to help. I'm already memoizing the
I'm happy to answer any questions, and my intent here is to be as helpful as possible, but I have already spent a lot of time creating a minimal reproduction as the issue template asked. I agree the issue is confusing, but I have made it as simple as possible. I've got a basic minimal query, and upon loading the page the query runs multiple times instead of once. I think it's reasonable to wait on your end until we rule out any bug in Reacts end here facebook/react#24155 -- if it turns out this is a bug in React, any discussion about |
okay, I debugged your example a little bit. The issue arises also in react 17, so it has nothing to do with concurrent mode at least. It's also independent of
while what we do in the code might seem arbitrary, it is not. staleTime only starts to kick in after the query is done fetching, so even if you fetch takes 5 seconds, a staleTime of 1 second is just fine. you can try that out by leaving out the the default staleTime: 0 works in normal, non-suspend mode because we mount our observer, which kicks off the fetching, and it stays mounted. However, with suspense, the component mounts, and then unmounts, because the promise is thrown to the suspense boundary. Then, when the fetching finishes, the component mounts again. Due to that's why we set the 1s staleTime. I think we just didn't account for the fact that someone might pass in What we should do is change that code to take a minimum of 1second staleTime for suspense instead I think. |
It sounds like you've isolated the bug pretty good, thank you for doing that investigation!
I had assumed as much. I guess I was theorizing that maybe there is a race condition between the query becoming stale and how long it takes react to render (after the query is done fetching). I'm just guessing, though.
I see how you could have missed this because I was not super concise in my communication, but in this comment I had mentioned I tried changing Thank you again for looking into my issue report! Maybe another possible solution is disable |
I just trie out your example repo, with react18 and react17, where |
Yes, I can confirm the same upon double checking. However, I see the component is still updated multiple times outside of the transition, but if I add on the const result = useQuery(
"foo",
mockFetcher,
{ suspense: true, staleTime: Infinity, cacheTime: 0 }
);
const data = useMemo(()=>{
console.log('data changed', result.data)
return result.data
}, [result.data])
console.log('result changed', result); When i hit the button to do the React 18 transition, I get EDIT looks like I still get the |
Ok I updated the example. In my real project, I have variables being passed into the query, which are stored in React state inside the same component calling In the minimal reproduction example, I tried to create a similar bug by adding: const [variables, setVariables] = useState(() => Math.random()); I guess because we're doing "fetch on render", each time the component suspends the state is lost. Therefore, the state initializer runs when it goes to render again after having previously suspended. If the state is initialized differently than the previous render that suspended, it triggers the side effect again, which suspends again. Since I've used They're saying that for now one of the only "supported" patterns is to create the promise above the suspense boundary, and store it in state (and not memo). Since we're creating it within the suspense boundary here, both state and memo are blown away due to how suspense works. If it were created above the suspense boundary, at least state should work I believe. I guess one workaround could be to create my variables above the suspense boundary, but that doesn't seem ideal. |
It also worth mentioning in my real app these variables in the URL don’t actually change between the two renders, but since I consume the URL in useMemo and my memoization is lost when suspending, react-query is given variables object that is equal by value but not reference. In the real app it triggers “data changed” only 2x, as compared to the minimal repro which logs “data changed” infinitely, but in both cases the general idea is that “fetch on render” is error prone in general in react. |
It is working now, the only action item on your end is to perhaps consider instructing your users to set Thanks for your help in narrowing this down! |
Describe the bug
My team is developing an app where we have an endpoint that returns mocked out random data on each request. This made a subtle bug in
react-query
super obvious to us.There is non determinism where
react-query
will sometimes send the query multiple times. Also, if you have auseTransition
hook (React concurrent mode), theuseQuery
is updating the consuming component outside of the transition which is bad as it blocks the main thread.I suspect this is partly because strict mode in React 18 unmounts and mounts the component, see reactwg/react-18#19
I think the bug in
react-query
can happen even without strict mode enabled. I read your source code and when your user setscacheTime
to 0, under the hood you guys set it to 1. For "stale time" you set it to 1000. https://github.com/tannerlinsley/react-query/blob/b44c213b39a258486aff719ba87a5f7d8c57fab4/src/react/useBaseQuery.ts#L60-L66 which was done in this PR #2821This implementation under the hood is concerning and I suspect it is related to the bug. Depending on whether React takes more or less time than the hard coded cache/stale times you've selected for us, it determines how many times the query will run and whether it will re-run outside of
startTransition
.Your minimal, reproducible example
https://github.com/joshribakoff-sm/rq-bug
Steps to reproduce
npm i [email protected] --force
,npm i [email protected] --force
react-query
and suppress peer dependency errornpm i react-query --legacy-peer-deps
src/index.js
const queryClient = new QueryClient({suspense: true})
,<QueryClientProvider client={queryClient}><App /></QueryClientProvider>
Expected behavior
I expect the app to suspend or start a transition, and run the query once. I expect never to actually render with "stale" data.
Instead, the query itself runs multiple times even on the initial page load (not deterministic). Sometimes the component is rendered as many as 5+ times. In a more complex example you can observe the updates are happing outside of the transition, which can also be verified in React profiler.
How often does this bug happen?
Often
Screenshots or Videos
No response
Platform
Mac, chrome
react-query version
3.34
TypeScript version
No response
Additional context
Suspense support honestly seems a little half baked in react-query, I would suggest to label your support for it as experimental (not just that react suspense itself is experimental).
Can we get a way to set a "resource" object like in the react docs example? https://codesandbox.io/s/vigilant-feynman-kpjy8w?file=/src/index.js
The resource object is set into state, and passed down. Only the child that calls
resource.read()
suspends. This avoids the mind bending complexity of cacheTime/staleTime race conditions when the parent which starts the query inevitably suspends and re-mounts.When suspense is enabled, you still return to us a bunch of things like
isLoading
andisStale
which is useless as React itself handles keeping stale results on screen now (transition) and loading states (suspense), and this seems to confuse developers who sometimes mix approaches that are incompatible. I would propose an entirely new hook built from the ground up just for the new paradigm, without any bloat. This new hook would neverthrow
, instead it would return theresource
object which would itself throw when one callsresource.read()
[after passing it down the tree]The text was updated successfully, but these errors were encountered: