reactjsreact-query

React query useSuspenseQuery causes twice render when using state-changing component


I am using useSuspenseQuery from React Query to fetch data in a React app. While the API is fetching, I want to show a loading state using <Suspense fallback="Loading" />.

The issue occurs when I include a Header component that updates its state with setTimeout. In this case, the Body component, which contains the query, renders twice instead of once. However, if I remove Header, the Body component renders only once, as expected.


Additional Findings

My Concerns

  1. Header and Body are sibling components, not a parent-child relationship. I don't understand why Body re-renders just because Header updates its state.
  2. I also don't understand why the timing of the API fetch and the state update in Header affects this behavior.

Code Example

App.tsx


import { Suspense } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import Header from "./components/Header";
import Body from "./components/Body";

export default function App() {
  const queryClient = new QueryClient();

  return (
    <QueryClientProvider client={queryClient}>

        <Header />

        <Suspense fallback={<div>Loading</div>}>
          <Body />
        </Suspense>

    </QueryClientProvider>
  );
}

Header.tsx


import React, { useEffect, useState } from "react";

const Header =() => {
  const [change, setChange] = useState(false);
  console.log("Header chang", change);

  useEffect(() => {
    setTimeout(() => {
      setChange(true);
    }, 250);
  }, []);

  return <h1 style={{ color: change ? "blue" : "red" }}>Header</h1>;
};

export default Header;

Body.tsx

import { useSuspenseQuery } from '@tanstack/react-query';

let count = 0;
const Body = () => {
  const { data } = useSuspenseQuery({
    queryKey: ['body'],
    queryFn: async () => {
      //api delay
      await new Promise((r) => setTimeout(r, 200));
      return {'message':'success'}
    },
  });
  console.log('Body', count++);

  return (
      <div>{count}</div>
  );
};

export default Body;

Expected Logs

(Regardless of where Header change: true appears)

Header change :  false
api done
Body render 0
Header change :  true

Actual Logs (When Header is included)

Header change :  false
api done
Body render 0
Header change :  true
Body render 1  // Unexpected extra render

It seems like Body is rendering twice when Header updates its state.


Demo

You can try this issue in the sandbox:
🔗 https://codesandbox.io/p/sandbox/react-query-suspense-twice-7t2rx7

Note:


Questions

  1. Why does Body re-render when Header updates its state, even though they are sibling components and not directly connected?

  2. Why does the timing of the API fetch and setTimeout affect this behavior?

  3. How can I prevent Body from rendering twice while keeping Suspense for useSuspenseQuery?


Solution

  • Fascinating issue. It doesn’t appear when we downgrade to React18. I think it could be related to the pre-warming of suspense siblings in v19. But we’d have to ask the react team. Could also be a legit bug in react.