-
Notifications
You must be signed in to change notification settings - Fork 48.5k
React 18 - infinite loop and function as child issue with transition and suspense with useMemo #24155
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
Comments
This is your original example: https://codesandbox.io/s/dazzling-cherry-5bsiv6?file=/src/App.js I can reproduce this error:
Like the error says, the issue is that you are trying to render a function: <Suspense fallback={() => <div>fallback</div>}> This line should be instead: <Suspense fallback={<div>fallback</div>}> This is the version where this mistake is fixed (in both places where the original code had it): https://codesandbox.io/s/vigorous-cloud-clwtvt?file=/src/App.js I can observe that after clicking "Start transition", there is a log that repeats every three seconds. I assume this is the issue you are referring to as the loop. I'll need to look a bit more to understand why this doesn't work. However, in general, Suspense relies on having an external cache. If you're creating "resources" during render (whether with useMemo or useState), you will likely run into issues. |
Thanks Dan. Yes, fetch as you render is working great by following the code sandbox examples in the React docs. We wanted to explore "fetch on render" which was where we got the infinite loop. We think there are sometimes use cases for both patterns, although we can only get one of them to work. Having an example of "fetch on render" would probably be very helpful for library authors, or if its a pattern that won't work, clarifying this would be great! Thank you again! |
Even with "fetch on render", the intention is that both the pending Promise and the eventual result (after its completion) are stored outside of your components. Even if that Map is lazily filled instead of being created preemptively. This could be a manual sideways cache for now. (Of course, this poses tricky questions about invalidation.) That's how Relay Suspense integration works today. In the future, we'd like to build primitives to make this easy: reactwg/react-18#25. |
I assumed that to satisfy the requirement of storing the Promise "externally", it would suffice to do so above the suspense boundary. I was expecting only the child to suspend and not the parent, and if the parent does not suspend I am confused why it would lose it's state. In either case, working examples would be very helpful! I'll review the linked discussion, thank you for that context as well. |
Here is a smaller example of the same loop: https://codesandbox.io/s/serverless-wave-469uso?file=/src/App.js The issue here is that when you suspend and React doesn't have anything useful to commit (because you're in a transition, so there is no fallback loading state to show), the whole result of that render gets thrown away. This includes anything that happened during render, no matter how high in the tree. This includes any new memoized results. So
This can work in a limited way, but any information you rely on has to be calculated outside of render. This is why storing a resource in state works (it won't get dropped) but creating a resource during render doesn't (it will get dropped). |
So, to sum up the recommendation:
|
Thank you, it feels great to get such a concise and quick response. I was very keen to understand the "rules" here, and now I understand perfectly. This will be very useful context to disseminate once the release is stable and the docs are updated on the React site! |
We think it’s still pretty hard to do a hand-rolled cache correctly. It’s not clear what’s the right place to document it until the built-in Cache is available. We don’t want to give an impression that now is the best time to try to write a Suspense integration for a data fetching library. |
Totally makes sense. For now we decided to use the "compromise" solution you mentioned, so we're setting the "resource" into state above the suspense boundary and this works perfectly 👨🍳 We're only using it one place, rendering a bunch of expensive SVG, so we're using an escape hatch since it's so impactful for our UX :) |
I've turned off strict mode, and tried to create a simple example that breaks in v18 rc3.
I'm trying to use
useMemo
to detect when some state changes and create a new memoized promise. AnotheruseMemo
call detects when the promise changes and wraps it in a "resource" object. I am passing the resource down to a child. There are multiple suspense boundaries between where I am creating the "resource" and where I'm calling.read()
. I expect this to load once with no errors, and when I click the button I expect a single transition. Instead, although the app loads I get this errorreact-dom.development.js:86 Warning: Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from render. Or maybe you meant to call this function
. When I click the button, it then goes into an infinite loop. This is unexpected because<App>
should not be suspending, only<Child>
. I'm also not returning any components without calling them from what I can tell.If I set the "resource" into state it works fine. This only seems to happen if I try to create the "resource" with
useMemo
.Minimal reproduction case
The text was updated successfully, but these errors were encountered: