Skip to content

multiple updates outside of react transition when using suspense #3432

@joshribakoff-sm

Description

@joshribakoff-sm

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-query and suppress peer dependency error npm 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]

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions