-
-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Description
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 a useTransition hook (React concurrent mode), the useQuery 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
With the release of React 18, StrictMode gets an additional behavior that we call “strict effects” mode. When strict effects are enabled, React intentionally double-invokes effects (mount -> unmount -> mount) for newly mounted components. Like other strict mode behaviors, React will only do this for development builds.
I think the bug in react-query can happen even without strict mode enabled. I read your source code and when your user sets cacheTime 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 #2821
This 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
- Upgrade to react 18 rc
npm i [email protected] --force,npm i [email protected] --force - Install
react-queryand suppress peer dependency errornpm i react-query --legacy-peer-deps - Verify strict mode is on (default in create react app)
src/index.js - Wrap the app in query client with suspense mode
const queryClient = new QueryClient({suspense: true}),<QueryClientProvider client={queryClient}><App /></QueryClientProvider> - Create a simple app that loads random data in a query, and triggers the query within a react transition:
function App() {
const [isPending, startTransition] = useTransition();
const result = useQuery(
"foo",
async () => {
console.log('query running')
await new Promise((res) => setTimeout(res, 1000));
return () => "foo" + Math.random();
},
{ suspense: true }
);
console.log(result);
return (
<div className="App">
{JSON.stringify(result)}
<button
onClick={() => {
startTransition(() => {
result.refetch();
});
}}
>
fetch
</button>
</div>
);
}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 and isStale 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 never throw, instead it would return the resource object which would itself throw when one calls resource.read() [after passing it down the tree]