Join GitHub today
GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.
Sign upGitHub is where the world builds software
Millions of developers and companies build, ship, and maintain their software on GitHub — the largest and most advanced development platform in the world.
Bugfix: Do not unhide a suspended tree without finishing the suspended update #18411
Conversation
When we commit a fallback, we cannot unhide the content without including the level that originally suspended. That's because the work at level outside the boundary (i.e. everything that wasn't hidden during that render) already committed.
|
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit b225cc1:
|
Details of bundled changes.Comparing: 35a2f74...b225cc1 react-dom
react-native-renderer
react-art
react-test-renderer
react-reconciler
ReactDOM: size: 0.0%, gzip: -0.1% Size changes (stable) |
Details of bundled changes.Comparing: 35a2f74...b225cc1 react-test-renderer
react-dom
react-art
react-native-renderer
react-reconciler
ReactDOM: size: 0.0%, gzip: -0.0% Size changes (experimental) |
This comment has been minimized.
This comment has been minimized.
|
Deployment has failed due to an internal error. (code: Contact our support with support@zeit.co for more information. |
d7382b6
into
facebook:master
I think this was just poor factoring on my part in facebook#18411. Honestly it doesn't make much sense to me, but my best guess is that I must have though that when `baseTime > currentChildExpirationTime`, the function would fall through to the `currentChildExpirationTime < renderExpirationTime` branch below. Really I think just made an oopsie. Regardless, this logic is galaxy brainéd. A goal of the Lanes refactor I'm working on is to make these types of checks -- is there remaining work in this tree? -- a lot easier to think about. Hopefully.
I think this was just poor factoring on my part in facebook#18411. Honestly it doesn't make much sense to me, but my best guess is that I must have thought that when `baseTime > currentChildExpirationTime`, the function would fall through to the `currentChildExpirationTime < renderExpirationTime` branch below. Really I think just made an oopsie. Regardless, this logic is galaxy brainéd. A goal of the Lanes refactor I'm working on is to make these types of checks -- is there remaining work in this tree? -- a lot easier to think about. Hopefully.
* Failing test for #18657 * Remove incorrect priority check I think this was just poor factoring on my part in #18411. Honestly it doesn't make much sense to me, but my best guess is that I must have thought that when `baseTime > currentChildExpirationTime`, the function would fall through to the `currentChildExpirationTime < renderExpirationTime` branch below. Really I think just made an oopsie. Regardless, this logic is galaxy brainéd. A goal of the Lanes refactor I'm working on is to make these types of checks -- is there remaining work in this tree? -- a lot easier to think about. Hopefully.
When a Suspense boundary is in its fallback state, you cannot switch back to the main content without also finishing any updates inside the tree that might have been skipped. That would be a form of tearing. Before we fixed this in facebook#18411, the way this bug manfiested was that a boundary was suspended by an update that originated from a child component (as opposed to props from a parent). While the fallback was showing, it received another update, this time at high priority. React would render the high priority update without also including the original update. That would cause the fallback to switch back to the main content, since the update that caused the tree to suspend was no longer part of the render. But then, React would immediately try to render the original update, which would again suspend and show the fallback, leading to a momentary flicker in the UI. The approach added in facebook#18411 is, when receiving a high priority update to a Suspense tree that's in its fallback state is to bail out, keep showing the fallback and finish the update in the rest of the tree. After that commits, render again at the original priority. Because low priority expiration times are inclusive of higher priority expiration times, this ensures that all the updates are committed together. The new approach in this commit is to turn `renderExpirationTime` into a context-like value that lives on the stack. Then, when unhiding the Suspense boundary, we can push a new `renderExpirationTime` that is inclusive of both the high pri update and the original update that suspended. Then the boundary can be unblocked in a single render pass. An advantage of the old approach is that by deferring the work of unhiding, there's less work to do in the high priority update. The key advantage of the new approach is that it solves the consistency problem without having to entangle the entire root.
When a Suspense boundary is in its fallback state, you cannot switch back to the main content without also finishing any updates inside the tree that might have been skipped. That would be a form of tearing. Before we fixed this in facebook#18411, the way this bug manifested was that a boundary was suspended by an update that originated from a child component (as opposed to props from a parent). While the fallback was showing, it received another update, this time at high priority. React would render the high priority update without also including the original update. That would cause the fallback to switch back to the main content, since the update that caused the tree to suspend was no longer part of the render. But then, React would immediately try to render the original update, which would again suspend and show the fallback, leading to a momentary flicker in the UI. The approach added in facebook#18411 is, when receiving a high priority update to a Suspense tree that's in its fallback state is to bail out, keep showing the fallback and finish the update in the rest of the tree. After that commits, render again at the original priority. Because low priority expiration times are inclusive of higher priority expiration times, this ensures that all the updates are committed together. The new approach in this commit is to turn `renderExpirationTime` into a context-like value that lives on the stack. Then, when unhiding the Suspense boundary, we can push a new `renderExpirationTime` that is inclusive of both the high pri update and the original update that suspended. Then the boundary can be unblocked in a single render pass. An advantage of the old approach is that by deferring the work of unhiding, there's less work to do in the high priority update. The key advantage of the new approach is that it solves the consistency problem without having to entangle the entire root.
When a Suspense boundary is in its fallback state, you cannot switch back to the main content without also finishing any updates inside the tree that might have been skipped. That would be a form of tearing. Before we fixed this in facebook#18411, the way this bug manifested was that a boundary was suspended by an update that originated from a child component (as opposed to props from a parent). While the fallback was showing, it received another update, this time at high priority. React would render the high priority update without also including the original update. That would cause the fallback to switch back to the main content, since the update that caused the tree to suspend was no longer part of the render. But then, React would immediately try to render the original update, which would again suspend and show the fallback, leading to a momentary flicker in the UI. The approach added in facebook#18411 is, when receiving a high priority update to a Suspense tree that's in its fallback state is to bail out, keep showing the fallback and finish the update in the rest of the tree. After that commits, render again at the original priority. Because low priority expiration times are inclusive of higher priority expiration times, this ensures that all the updates are committed together. The new approach in this commit is to turn `renderExpirationTime` into a context-like value that lives on the stack. Then, when unhiding the Suspense boundary, we can push a new `renderExpirationTime` that is inclusive of both the high pri update and the original update that suspended. Then the boundary can be unblocked in a single render pass. An advantage of the old approach is that by deferring the work of unhiding, there's less work to do in the high priority update. The key advantage of the new approach is that it solves the consistency problem without having to entangle the entire root.
When a Suspense boundary is in its fallback state, you cannot switch back to the main content without also finishing any updates inside the tree that might have been skipped. That would be a form of tearing. Before we fixed this in facebook#18411, the way this bug manifested was that a boundary was suspended by an update that originated from a child component (as opposed to props from a parent). While the fallback was showing, it received another update, this time at high priority. React would render the high priority update without also including the original update. That would cause the fallback to switch back to the main content, since the update that caused the tree to suspend was no longer part of the render. But then, React would immediately try to render the original update, which would again suspend and show the fallback, leading to a momentary flicker in the UI. The approach added in facebook#18411 is, when receiving a high priority update to a Suspense tree that's in its fallback state is to bail out, keep showing the fallback and finish the update in the rest of the tree. After that commits, render again at the original priority. Because low priority expiration times are inclusive of higher priority expiration times, this ensures that all the updates are committed together. The new approach in this commit is to turn `renderExpirationTime` into a context-like value that lives on the stack. Then, when unhiding the Suspense boundary, we can push a new `renderExpirationTime` that is inclusive of both the high pri update and the original update that suspended. Then the boundary can be unblocked in a single render pass. An advantage of the old approach is that by deferring the work of unhiding, there's less work to do in the high priority update. The key advantage of the new approach is that it solves the consistency problem without having to entangle the entire root.
When a Suspense boundary is in its fallback state, you cannot switch back to the main content without also finishing any updates inside the tree that might have been skipped. That would be a form of tearing. Before we fixed this in facebook#18411, the way this bug manifested was that a boundary was suspended by an update that originated from a child component (as opposed to props from a parent). While the fallback was showing, it received another update, this time at high priority. React would render the high priority update without also including the original update. That would cause the fallback to switch back to the main content, since the update that caused the tree to suspend was no longer part of the render. But then, React would immediately try to render the original update, which would again suspend and show the fallback, leading to a momentary flicker in the UI. The approach added in facebook#18411 is, when receiving a high priority update to a Suspense tree that's in its fallback state is to bail out, keep showing the fallback and finish the update in the rest of the tree. After that commits, render again at the original priority. Because low priority expiration times are inclusive of higher priority expiration times, this ensures that all the updates are committed together. The new approach in this commit is to turn `renderExpirationTime` into a context-like value that lives on the stack. Then, when unhiding the Suspense boundary, we can push a new `renderExpirationTime` that is inclusive of both the high pri update and the original update that suspended. Then the boundary can be unblocked in a single render pass. An advantage of the old approach is that by deferring the work of unhiding, there's less work to do in the high priority update. The key advantage of the new approach is that it solves the consistency problem without having to entangle the entire root.
* Unhide Suspense trees without entanglement When a Suspense boundary is in its fallback state, you cannot switch back to the main content without also finishing any updates inside the tree that might have been skipped. That would be a form of tearing. Before we fixed this in #18411, the way this bug manifested was that a boundary was suspended by an update that originated from a child component (as opposed to props from a parent). While the fallback was showing, it received another update, this time at high priority. React would render the high priority update without also including the original update. That would cause the fallback to switch back to the main content, since the update that caused the tree to suspend was no longer part of the render. But then, React would immediately try to render the original update, which would again suspend and show the fallback, leading to a momentary flicker in the UI. The approach added in #18411 is, when receiving a high priority update to a Suspense tree that's in its fallback state is to bail out, keep showing the fallback and finish the update in the rest of the tree. After that commits, render again at the original priority. Because low priority expiration times are inclusive of higher priority expiration times, this ensures that all the updates are committed together. The new approach in this commit is to turn `renderExpirationTime` into a context-like value that lives on the stack. Then, when unhiding the Suspense boundary, we can push a new `renderExpirationTime` that is inclusive of both the high pri update and the original update that suspended. Then the boundary can be unblocked in a single render pass. An advantage of the old approach is that by deferring the work of unhiding, there's less work to do in the high priority update. The key advantage of the new approach is that it solves the consistency problem without having to entangle the entire root. * Create internal LegacyHidden type This only exists so we can clean up the internal implementation of `<div hidden={isHidden} />`, which is not a stable feature. The goal is to move everything to the new Offscreen type instead. However, Offscreen has different semantics, so before we can remove the legacy API, we have to migrate our internal usage at Facebook. So we'll need to maintain both temporarily. In this initial commit, I've only added the type. It's not used anywhere. The next step is to use it to implement `hidden`. * Use LegacyHidden to implement old hidden API If a host component receives a `hidden` prop, we wrap its children in an Offscreen fiber. This is similar to what we do for Suspense children. The LegacyHidden type happens to share the same implementation as the new Offscreen type, for now, but using separate types allows us to fork the behavior later when we implement our planned changes to the Offscreen API. There are two subtle semantic changes here. One is that the children of the host component will have their visibility toggled using the same mechanism we use for Offscreen and Suspense: find the nearest host node children and give them a style of `display: none`. We didn't used to do this in the old API, because the `hidden` DOM attribute on the parent already hides them. So with this change, we're actually "overhiding" the children. I considered addressing this, but I figure I'll leave it as-is in case we want to expose the LegacyHidden component type temporarily to ease migration of Facebook's internal callers to the Offscreen type. The other subtle semantic change is that, because of the extra fiber that wraps around the children, this pattern will cause the children to lose state: ```js return isHidden ? <div hidden={true} /> : <div />; ``` The reason is that I didn't want to wrap every single host component in an extra fiber. So I only wrap them if a `hidden` prop exists. In the above example, that means the children are conditionally wrapped in an extra fiber, so they don't line up during reconciliation, so they get remounted every time `isHidden` changes. The fix is to rewrite to: ```js return <div hidden={isHidden} />; ``` I don't anticipate this will be a problem at Facebook, especially since we're only supposed to use `hidden` via a userspace wrapper component. (And since the bad pattern isn't very React-y, anyway.) Again, the eventual goal is to delete this completely and replace it with Offscreen.
Fixes #18357
When we commit a fallback, we cannot unhide the content without including the level that originally suspended. That's because the work at level outside the boundary (i.e. everything that wasn't hidden during that render) already committed.