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

Bug: Context provider updates forcing Suspense fallback on hydration #24476

Closed
liuyenwei opened this issue May 2, 2022 · 12 comments
Closed

Bug: Context provider updates forcing Suspense fallback on hydration #24476

liuyenwei opened this issue May 2, 2022 · 12 comments
Labels
Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug

Comments

@liuyenwei
Copy link
Contributor

React version: 18.1.0

Steps To Reproduce

  1. Set up a ContextProvider and pass object as value (not-memoized)
  2. Wrap the ContextProvider around a Suspense boundary
  3. In a component, outside of the Suspense boundary, update the context value inside of a useEffect
  4. Notice that the de-hydrated suspense content rendered by the server now switches back to the fallback on the client

Link to code example:
https://codesandbox.io/s/react-18-redux-ssr-forked-rbkvtv?file=/src/App.js (forked from a redux ssr sandbox bc I originally thought this was a hydration issue with redux)

The current behavior

The update to the context provider causes the suspense boundary to switch to the fallback since it has not finished hydrating yet and shows this error:
image

The expected behavior

The suspense boundary is able to hydrate without switching to the fallback on the client.

Note

Updating the ContextProvider to memoize the value using useMemo addresses the issue but I am still curious if this is expected behavior or a bug.

@liuyenwei liuyenwei added the Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug label May 2, 2022
@nemanjam
Copy link

nemanjam commented May 4, 2022

I described exact same problem in this topic:

vercel/next.js#36582

My project uses Next.js and React Query. Can you post a code snippet how exactly you solved it with useMemo? React Query has it's own cache and I still get this error.

@acdlite
Copy link
Collaborator

acdlite commented May 16, 2022

This is intentional behavior: if a tree hasn't finished hydrating yet, and it receives an urgent update via either props or context, React will give up hydrating and render the update instead.

This is considered a "recoverable error" because the app doesn't crash, it just discards the HTML and reverts to client-only rendering. But this can have performance implications, so we log it anyway so the developer knows it needs to be fixed.

There are a few ways you can fix it, depending on the use case:

  • Avoid calling setState in useEffect, if appropriate. Whether this is viable really depends on what your use case is, though, so you have more details please share them.
  • Memoize the children using useMemo or memo so that React knows it doesn't affect the not-yet-hydrated subtree (this is the fix you've chosen, which is good).
  • Wrap the update in startTransition to tell React that it's not urgent. Then it can hydrate the children before applying the update. This is what the error message recommends because it works even if the update does flow into the children. And it's good practice, anyway.

Does this make sense?

We're working on ways to make the developer experience better, like including the name of the component that caused the update.

Also, if you could share more details on what you were trying to achieve when you hit this issue that could be helpful for us when communicating about this in the future.

@nemanjam
Copy link

This is very difficult error to resolve, it occurs so randomly. I memoized children, it occurs less frequently but still happens sometimes on page refresh. I don't have access to original state update, it happens in useQuery hook from React Query.

@acdlite
Copy link
Collaborator

acdlite commented May 24, 2022

I don't have access to original state update, it happens in useQuery hook from React Query.

Yeah memoizing the children doesn't work if a prop actually has changed. If the update is coming from somewhere in React Query you may need to ask them if they can provide a way to wrap data updates in startTransition.

@acdlite
Copy link
Collaborator

acdlite commented May 24, 2022

Btw we agree with your feedback that it's a difficult issue to debug. We have some ideas for how to improve the developer experience, like showing the name of the component or hook that triggered the update, or integrating with React DevTools.

@rickhanlonii
Copy link
Member

@nemanjam I'll take your thumbs up to indicate that this can be closed, let me know if that's wrong.

@OliverJAsh
Copy link

@acdlite Whilst we wait for the developer experience to be improved, do you have any tips to help debug these error messages?

This Suspense boundary received an update before it finished hydrating. This caused the boundary to switch to client rendering. The usual way to fix this is to wrap the original update in startTransition.

At Unsplash we're trying to migrate from renderToString + react-universal-component (link) to renderToPipeableStream + React.lazy + Suspense. However we are seeing a lot of these errors and we're really struggling to understand where they are coming from. Without more information in the error message I'm not sure we'll be able to continue this migration—we're stuck using synchronous renderToString.

@gaearon
Copy link
Collaborator

gaearon commented Oct 4, 2022

I think the stack trace ends with the setState call triggering it. Is that not the case?

@OliverJAsh
Copy link

Not as far as I can see:

image

@gaearon
Copy link
Collaborator

gaearon commented Oct 4, 2022

Hmm I see.

Until we have something better, some ways you can try debugging it:

  • Add a logpoint to scheduleUpdateOnFiber in react-dom.development.js. This gives you all updates that happened. You can log fiber.type. One of these might be triggering the issue.
  • Add a logpoint to propagateContextChange in react-dom.development.js. You can log context and/or workInProgress._debugOwner.type.

@OliverJAsh
Copy link

I'm running into a very similar issue when using react-redux, and unfortunately startTransition doesn't seem to help: reduxjs/react-redux#1962

@max-ch9i
Copy link

max-ch9i commented Dec 21, 2022

I'm facing a similar issue with Suspense and useSyncExternalStore. vercel/next.js#43920 The original issue was that I couldn't use zustand store, which uses useSyncExternalStore under the hood, inside a component wrapped in Suspense.

Copying here a stackblitz to reproduce: https://stackblitz.com/edit/nextjs-yuqb6g?file=app%2FPageComponents.js

In the example, I provide a getServerSnapshot that is called before hydration and whose value matches the value from SSR. And yet the hydration error is still thrown.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug
Projects
None yet
Development

No branches or pull requests

7 participants