-
Notifications
You must be signed in to change notification settings - Fork 46.9k
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
Fix: useDeferredValue initialValue suspends forever without switching to final #27888
Conversation
Fixes a bug in the experimental `initialValue` option for `useDeferredValue` (added in facebook#27500). If rendering the `initialValue` causes the tree to suspend, React should skip it and switch to rendering the final value instead. It should not wait for `initialValue` to resolve. This is not just an optimization, because in some cases the initial value may _never_ resolve — intentionally. For example, if the application does not provide an instant fallback state. This capability is, in fact, the primary motivation for the `initialValue` API. I mostly implemented this correctly in the original PR, but I missed some cases where it wasn't working: - If there's no Suspense boundary between the `useDeferredValue` hook and the component that suspends, and we're not in the shell of the transition (i.e. there's a parent Suspense boundary wrapping the `useDeferredValue` hook), the deferred task would get incorrectly dropped. - Similarly, if there's no Suspense boundary between the `useDeferredValue` hook and the component that suspends, and we're rendering a synchronous update, the deferred task would get incorrectly dropped. What these cases have in common is that it causes the `useDeferredValue` hook itself to be replaced by a Suspense fallback. The fix was the same for both. (It already worked in cases where there's no Suspense fallback at all, because those are handled differently, at the root.) The way I discovered this was when investigating a particular bug in Next.js that would happen during a 'popstate' transition (back/forward), but not during a regular navigation. That's because we render popstate transitions synchronously to preserve browser's scroll position — which in this case triggered the second scenario above.
Comparing: 1d5667a...61cde78 Critical size changesIncludes critical production bundles, as well as any change greater than 2%:
Significant size changesIncludes any change greater than 0.2%: Expand to show |
@@ -41,6 +41,7 @@ export const StoreConsistency = /* */ 0b0000000000000100000000000000 | |||
// possible, because we're about to run out of bits. | |||
export const ScheduleRetry = StoreConsistency; | |||
export const ShouldSuspendCommit = Visibility; | |||
export const DidDefer = ContentReset; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really know how we use ContentReset but I assume you've considered that whether this ever propagates through subtree flags and that the reuse can't inadvertantly mix flags?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I picked one that wouldn’t conflict since we’re almost out of bits.
… to final (#27888) Fixes a bug in the experimental `initialValue` option for `useDeferredValue` (added in #27500). If rendering the `initialValue` causes the tree to suspend, React should skip it and switch to rendering the final value instead. It should not wait for `initialValue` to resolve. This is not just an optimization, because in some cases the initial value may _never_ resolve — intentionally. For example, if the application does not provide an instant fallback state. This capability is, in fact, the primary motivation for the `initialValue` API. I mostly implemented this correctly in the original PR, but I missed some cases where it wasn't working: - If there's no Suspense boundary between the `useDeferredValue` hook and the component that suspends, and we're not in the shell of the transition (i.e. there's a parent Suspense boundary wrapping the `useDeferredValue` hook), the deferred task would get incorrectly dropped. - Similarly, if there's no Suspense boundary between the `useDeferredValue` hook and the component that suspends, and we're rendering a synchronous update, the deferred task would get incorrectly dropped. What these cases have in common is that it causes the `useDeferredValue` hook itself to be replaced by a Suspense fallback. The fix was the same for both. (It already worked in cases where there's no Suspense fallback at all, because those are handled differently, at the root.) The way I discovered this was when investigating a particular bug in Next.js that would happen during a 'popstate' transition (back/forward), but not during a regular navigation. That's because we render popstate transitions synchronously to preserve browser's scroll position — which in this case triggered the second scenario above. DiffTrain build for [f1039be](f1039be)
### React upstream changes - facebook/react#27888 - facebook/react#27870 - facebook/react#27871 - facebook/react#27850 - facebook/react#27839 - facebook/react#27842 - facebook/react#27841 - facebook/react#27840 - facebook/react#27761 - facebook/react#27831 - facebook/react#27801 Closes NEXT-2012
… to final (facebook#27888) Fixes a bug in the experimental `initialValue` option for `useDeferredValue` (added in facebook#27500). If rendering the `initialValue` causes the tree to suspend, React should skip it and switch to rendering the final value instead. It should not wait for `initialValue` to resolve. This is not just an optimization, because in some cases the initial value may _never_ resolve — intentionally. For example, if the application does not provide an instant fallback state. This capability is, in fact, the primary motivation for the `initialValue` API. I mostly implemented this correctly in the original PR, but I missed some cases where it wasn't working: - If there's no Suspense boundary between the `useDeferredValue` hook and the component that suspends, and we're not in the shell of the transition (i.e. there's a parent Suspense boundary wrapping the `useDeferredValue` hook), the deferred task would get incorrectly dropped. - Similarly, if there's no Suspense boundary between the `useDeferredValue` hook and the component that suspends, and we're rendering a synchronous update, the deferred task would get incorrectly dropped. What these cases have in common is that it causes the `useDeferredValue` hook itself to be replaced by a Suspense fallback. The fix was the same for both. (It already worked in cases where there's no Suspense fallback at all, because those are handled differently, at the root.) The way I discovered this was when investigating a particular bug in Next.js that would happen during a 'popstate' transition (back/forward), but not during a regular navigation. That's because we render popstate transitions synchronously to preserve browser's scroll position — which in this case triggered the second scenario above.
… to final (#27888) Fixes a bug in the experimental `initialValue` option for `useDeferredValue` (added in #27500). If rendering the `initialValue` causes the tree to suspend, React should skip it and switch to rendering the final value instead. It should not wait for `initialValue` to resolve. This is not just an optimization, because in some cases the initial value may _never_ resolve — intentionally. For example, if the application does not provide an instant fallback state. This capability is, in fact, the primary motivation for the `initialValue` API. I mostly implemented this correctly in the original PR, but I missed some cases where it wasn't working: - If there's no Suspense boundary between the `useDeferredValue` hook and the component that suspends, and we're not in the shell of the transition (i.e. there's a parent Suspense boundary wrapping the `useDeferredValue` hook), the deferred task would get incorrectly dropped. - Similarly, if there's no Suspense boundary between the `useDeferredValue` hook and the component that suspends, and we're rendering a synchronous update, the deferred task would get incorrectly dropped. What these cases have in common is that it causes the `useDeferredValue` hook itself to be replaced by a Suspense fallback. The fix was the same for both. (It already worked in cases where there's no Suspense fallback at all, because those are handled differently, at the root.) The way I discovered this was when investigating a particular bug in Next.js that would happen during a 'popstate' transition (back/forward), but not during a regular navigation. That's because we render popstate transitions synchronously to preserve browser's scroll position — which in this case triggered the second scenario above. DiffTrain build for commit f1039be.
Fixes a bug in the experimental
initialValue
option foruseDeferredValue
(added in #27500).If rendering the
initialValue
causes the tree to suspend, React should skip it and switch to rendering the final value instead. It should not wait forinitialValue
to resolve.This is not just an optimization, because in some cases the initial value may never resolve — intentionally. For example, if the application does not provide an instant fallback state. This capability is, in fact, the primary motivation for the
initialValue
API.I mostly implemented this correctly in the original PR, but I missed some cases where it wasn't working:
useDeferredValue
hook and the component that suspends, and we're not in the shell of the transition (i.e. there's a parent Suspense boundary wrapping theuseDeferredValue
hook), the deferred task would get incorrectly dropped.useDeferredValue
hook and the component that suspends, and we're rendering a synchronous update, the deferred task would get incorrectly dropped.What these cases have in common is that it causes the
useDeferredValue
hook itself to be replaced by a Suspense fallback. The fix was the same for both. (It already worked in cases where there's no Suspense fallback at all, because those are handled differently, at the root.)The way I discovered this was when investigating a particular bug in Next.js that would happen during a 'popstate' transition (back/forward), but not during a regular navigation. That's because we render popstate transitions synchronously to preserve browser's scroll position — which in this case triggered the second scenario above.