Skip to content
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

NextJS page HydrationBoundary not working if prefetched query accessed via layout #8479

Closed
cbovis opened this issue Dec 28, 2024 · 1 comment

Comments

@cbovis
Copy link

cbovis commented Dec 28, 2024

Describe the bug

In our NextJS application (app router) we're prefetching the logged in user on all requests via prefetchQuery + HydrationBoundary in the global layout. Currently this opts us into dynamic rendering across the board and so I'm reworking this to be optional on a per-page basis.

We use the logged in user in our application header as well as for various page-specific tasks (e.g. determining whether to show sign up CTAs).

My plan was to move the prefetching into each individual page in our application via a higher order component. Then we can selectively remove that HOC on pages where we want to adopt static rendering. After doing this however I was seeing loading indicators show up across the board on first page load despite being able to debug that the prefetch logic is executing as expected before the initial response returns.

I believe I've tracked the issue down to using the prefetched query in a layout component higher in the tree than the page component which is doing the prefetching. When doing this both the layout consumer and page consumer cannot access the prefetched data on first render. It's understandable that the layout consumer doesn't have the data but I'd expect the page consumer to have access to it and render with that data immediately.

Your minimal, reproducible example

https://github.com/cbovis/rq-hydration-issue

Steps to reproduce

I've setup a simple NextJS application which demonstrates the issue in the simplest way possible.

  1. pnpm install all the dependencies
  2. Run pnpm dev to start up dev server
  3. Access http://localhost:3000 to see things working as expected
  4. Access http://localhost:3000/broken to see things broken

At http://localhost:3000 you can access dev tools then look at the HTML response for the page to see it returns with the prefetched data as expected (see screenshot). This page doesn't try to consume the query in its layout, only in the page.

At http://localhost:3000/broken you can look at the HTML response to see both the layout and page components render without the prefetched data (see screenshot). I've put in an artificial delay on the query and you can observe that the response does indeed wait on this. Instead of rendering with the data a loading state is shown for both consumers.

After loading /broken, some kind of hydration immediately swaps out the loading state for the correct value which suggests RQ does indeed have access to it and the data is crossing the client-server boundary. If RQ didn't have access to the data then the loading state would be shown for 5 seconds before the query resolves due to the artificial delay.

Expected behavior

I would expect the page to render with the prefetched data immediately and the layout to render in a loading state since the query is prefetched after the layout renders.

How often does this bug happen?

Every time

Screenshots or Videos

Image Image

Platform

In the reproduction repo:

"@tanstack/react-query": "^5.62.11"
"next": "15.1.3",
"prettier": "^3.4.2",
"react": "^19.0.0",
"react-dom": "^19.0.0"

Also occurring in our application which uses:

React 18
NextJS 14
React Query 5

Tanstack Query adapter

react-query

TanStack Query version

5.62.11

TypeScript version

No response

Additional context

No response

@TkDodo
Copy link
Collaborator

TkDodo commented Dec 28, 2024

I think this behaviour occurs because PrefetchIt is not wrapped in a suspense boundary, thus the “global” suspense boundary from next kicks in, which also wraps the layout.

wrapping either the children of the Home component in Suspense:

export default async function Home() {
  return (
   <Suspense fallback="home...">
      <PrefetchIt>
        <div>
          Page:
          <ShowIt />
        </div>
      </PrefetchIt>
    </Suspense>
  )
}

or the children in the layout in Suspense:

  return (
    <>
      <div>
        Layout: <ShowIt />
      </div>
      <Suspense fallback="layout...">{children}</Suspense>
    </>
  )

will do the trick

@TkDodo TkDodo closed this as not planned Won't fix, can't repro, duplicate, stale Dec 28, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants