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

Hide timed-out children instead of deleting them so their state is preserved #13823

Merged
merged 13 commits into from
Oct 18, 2018

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Oct 11, 2018

If the children timeout, we switch to showing the fallback children in place of the "primary" children. However, we don't want to delete the primary children because then their state will be lost (both the React state and the host state, e.g. uncontrolled form inputs). Instead we keep them mounted and hide them. Both the fallback children AND the primary children are rendered at the same time. Once the primary children are un-suspended, we can delete the fallback children — don't need to preserve their state.

The two sets of children are siblings in the host environment, but semantically, for purposes of reconciliation, they are two separate sets. So we store them using two fragment fibers.

However, we want to avoid allocating two extra fibers for every Suspense fiber. They're only necessary when the children time out, because that's the only time when both sets are mounted.

So, the extra fragment fibers are only used if the children time out. Otherwise, we render the primary children directly. This requires some custom reconciliation logic to preserve the state of the primary children. It's essentially a very basic form of re-parenting.

  • Reconcile fallback and primary children separately.
  • Consolidate various pieces of Suspense state into a single type stored on memoizedState.
  • Hide timed-out children by toggling their top-most host nodes.
  • React Native?

@sizebot
Copy link

sizebot commented Oct 11, 2018

ReactDOM: size: 🔺+2.2%, gzip: 🔺+2.4%

Details of bundled changes.

Comparing: 21a79a1...f06c005

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.development.js +2.6% +1.9% 660.48 KB 677.37 KB 154.35 KB 157.33 KB UMD_DEV
react-dom.production.min.js 🔺+2.2% 🔺+2.4% 95.72 KB 97.79 KB 31.18 KB 31.93 KB UMD_PROD
react-dom.development.js +2.6% +1.9% 655.82 KB 672.71 KB 152.97 KB 155.93 KB NODE_DEV
react-dom.production.min.js 🔺+2.2% 🔺+2.3% 95.71 KB 97.8 KB 30.77 KB 31.47 KB NODE_PROD
ReactDOM-dev.js +2.6% +2.0% 673.12 KB 690.62 KB 153.62 KB 156.62 KB FB_WWW_DEV
ReactDOM-prod.js 🔺+2.8% 🔺+2.2% 287.46 KB 295.46 KB 52.79 KB 53.96 KB FB_WWW_PROD
react-dom.profiling.min.js +1.7% +1.5% 98.5 KB 100.2 KB 31.33 KB 31.79 KB NODE_PROFILING
ReactDOM-profiling.js +2.1% +1.5% 293.66 KB 299.85 KB 54.16 KB 54.99 KB FB_WWW_PROFILING
react-dom.profiling.min.js +1.7% +1.6% 98.43 KB 100.13 KB 31.82 KB 32.33 KB UMD_PROFILING

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.development.js +3.6% +2.7% 448.46 KB 464.82 KB 100.42 KB 103.16 KB UMD_DEV
react-art.production.min.js 🔺+1.8% 🔺+2.0% 88.13 KB 89.73 KB 27.04 KB 27.57 KB UMD_PROD
react-art.development.js +4.3% +3.4% 380.25 KB 396.6 KB 83.25 KB 86.05 KB NODE_DEV
react-art.production.min.js 🔺+3.0% 🔺+3.2% 53.09 KB 54.71 KB 16.28 KB 16.81 KB NODE_PROD
ReactART-dev.js +4.4% +3.5% 383.96 KB 400.88 KB 81.66 KB 84.51 KB FB_WWW_DEV
ReactART-prod.js 🔺+3.6% 🔺+3.2% 162.78 KB 168.59 KB 27.42 KB 28.28 KB FB_WWW_PROD

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer.development.js +4.5% +3.5% 392.5 KB 410.06 KB 85.89 KB 88.87 KB UMD_DEV
react-test-renderer.production.min.js 🔺+3.4% 🔺+3.3% 54.34 KB 56.17 KB 16.64 KB 17.19 KB UMD_PROD
react-test-renderer.development.js +4.5% +3.5% 388.07 KB 405.64 KB 84.77 KB 87.76 KB NODE_DEV
react-test-renderer.production.min.js 🔺+3.4% 🔺+3.5% 54.04 KB 55.88 KB 16.42 KB 17 KB NODE_PROD
ReactTestRenderer-dev.js +4.6% +3.7% 392.17 KB 410.26 KB 83.52 KB 86.59 KB FB_WWW_DEV

react-noop-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-noop-renderer.development.js +16.4% +12.6% 23.96 KB 27.89 KB 5.41 KB 6.09 KB NODE_DEV
react-noop-renderer.production.min.js 🔺+18.3% 🔺+13.1% 8.92 KB 10.56 KB 3.11 KB 3.52 KB NODE_PROD
react-noop-renderer-persistent.development.js +16.3% +12.5% 24.08 KB 28.01 KB 5.42 KB 6.1 KB NODE_DEV
react-noop-renderer-persistent.production.min.js 🔺+18.3% 🔺+13.1% 8.95 KB 10.58 KB 3.11 KB 3.52 KB NODE_PROD

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler.development.js +4.4% +3.4% 377.85 KB 394.3 KB 81.69 KB 84.48 KB NODE_DEV
react-reconciler.production.min.js 🔺+5.6% 🔺+4.8% 52.82 KB 55.77 KB 15.84 KB 16.6 KB NODE_PROD
react-reconciler-persistent.development.js +4.4% +3.4% 376.46 KB 392.9 KB 81.13 KB 83.93 KB NODE_DEV
react-reconciler-persistent.production.min.js 🔺+5.6% 🔺+4.8% 52.83 KB 55.78 KB 15.84 KB 16.61 KB NODE_PROD

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactNativeRenderer-dev.js +3.3% +2.5% 510.41 KB 527.48 KB 112.65 KB 115.46 KB RN_FB_DEV
ReactNativeRenderer-prod.js 🔺+2.6% 🔺+2.1% 217.84 KB 223.43 KB 37.82 KB 38.61 KB RN_FB_PROD
ReactNativeRenderer-dev.js +3.3% +2.5% 510.12 KB 527.19 KB 112.57 KB 115.37 KB RN_OSS_DEV
ReactNativeRenderer-prod.js 🔺+2.6% 🔺+2.1% 217.85 KB 223.44 KB 37.82 KB 38.61 KB RN_OSS_PROD
ReactFabric-dev.js +3.4% +2.5% 500.57 KB 517.7 KB 110.23 KB 113.03 KB RN_FB_DEV
ReactFabric-prod.js 🔺+3.7% 🔺+2.5% 210.31 KB 218.01 KB 36.41 KB 37.34 KB RN_FB_PROD
ReactFabric-dev.js +3.4% +2.5% 500.6 KB 517.74 KB 110.25 KB 113.05 KB RN_OSS_DEV
ReactFabric-prod.js 🔺+3.7% 🔺+2.5% 210.35 KB 218.04 KB 36.43 KB 37.36 KB RN_OSS_PROD
ReactNativeRenderer-profiling.js +2.6% +2.1% 223.19 KB 229.03 KB 39.12 KB 39.96 KB RN_OSS_PROFILING
ReactFabric-profiling.js +3.6% +2.5% 215.06 KB 222.77 KB 37.67 KB 38.63 KB RN_OSS_PROFILING
ReactNativeRenderer-profiling.js +2.6% +2.1% 223.17 KB 229.01 KB 39.11 KB 39.95 KB RN_FB_PROFILING
ReactFabric-profiling.js +3.6% +2.5% 215.02 KB 222.73 KB 37.65 KB 38.61 KB RN_FB_PROFILING

scheduler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
scheduler.development.js n/a n/a 0 B 19.17 KB 0 B 5.74 KB UMD_DEV
scheduler.production.min.js n/a n/a 0 B 3.16 KB 0 B 1.53 KB UMD_PROD

Generated by 🚫 dangerJS

finishedWork.stateNode = {timedOutAt: currentTime};
// which the children timed out.
// $FlowFixMe - Intentionally using a value other than an UpdateQueue.
finishedWork.updateQueue = {timedOutAt: requestCurrentTime()};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we use this field for something other than the UpdateQueue, we must never use this field in any generic branches. I.e. we can't have any code that checks .updateQueue for a bunch of different or all tag types.

What's your motivation though? This doesn't make sense to me. It is a signal that stores a stateful value over time and never is allowed to be reset when fully processed. Sounds like state to me. Either stateNode or memoizedState.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that you mention it, memoizedState does seem to make the most sense. Good call.

@acdlite acdlite force-pushed the placeholder-children branch 3 times, most recently from d9b589b to b64734d Compare October 12, 2018 20:46
@acdlite acdlite changed the title [WIP] Hide timed-out children instead of deleting them so their state is preserved Hide timed-out children instead of deleting them so their state is preserved Oct 12, 2018
@acdlite acdlite requested a review from sebmarkbage October 12, 2018 20:48
@acdlite
Copy link
Collaborator Author

acdlite commented Oct 12, 2018

Ok this is ready for review. I don't know how to implement the hide/unhide methods for React Native so I've left those empty for now.

@acdlite acdlite force-pushed the placeholder-children branch 2 times, most recently from 7400fee to 46386e2 Compare October 12, 2018 21:12
@trueadm
Copy link
Contributor

trueadm commented Oct 12, 2018

So this approach uses display: none and display: block I see. Could we not just detach the DOM node from the document?

@acdlite
Copy link
Collaborator Author

acdlite commented Oct 12, 2018

So this approach uses display: none and display: block

When unhiding, it will either apply the user-provided display style prop, if it exists, or unset the display property using el.style.display = null:

export function unhideInstance(instance: Instance, props: Props): void {
instance = ((instance: any): HTMLElement);
if (instance.style !== undefined && instance.style !== null) {
let display = null;
if (props[STYLE] !== undefined && props[STYLE] !== null) {
const styleProp = props[STYLE];
if (styleProp.hasOwnProperty('display')) {
display = styleProp.display;
}
}
// $FlowFixMe Setting a style property to null is the valid way to reset it.
instance.style.display = display;
}
}

We considered detaching the DOM nodes but you lose more state with that approach. Like uncontrolled form inputs.

@trueadm
Copy link
Contributor

trueadm commented Oct 12, 2018

@acdlite I'm curious to what state we lose. I thought form elements (inputs, selects etc) keep their state when in a detached state. I see us running into user-land styling issues when playing with display when combined with CSS animations, grids and flexbox. At least, I have had countless issues in the past.

@acdlite
Copy link
Collaborator Author

acdlite commented Oct 12, 2018

@trueadm Oh maybe I'm wrong about that one. We lose iframe state, and I believe we lose both form inputs and video playback. I'll check again.

@trueadm
Copy link
Contributor

trueadm commented Oct 12, 2018

@acdlite I expect iframes to lose state, there was an earlier proposal to allow them to not do so but I think it was scrapped. Video elements shouldn't lose state anymore, they should pause if I remember from memory (apart from on iOS, which used to cause them to reload even if you apply display: none on them, you had to use visibility and change their position to absolute and off-screen). I guess no matter what approach, we'll have edge cases. This is probably one the community has researched in depth already and maybe we could reach out to see if anyone has some recent findings with examples.

@acdlite
Copy link
Collaborator Author

acdlite commented Oct 12, 2018

@trueadm Sounds good. I'll defer to you and @sebmarkbage's judgment.

@@ -371,6 +521,7 @@ function completeWork(
break;
}
case HostRoot: {
updateHostContainer(workInProgress);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is host environment throws, does it mean we don't clean up the stack due to pops being moved after?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll revert this, mistake when resolving conflicts

@gaearon
Copy link
Collaborator

gaearon commented Oct 12, 2018

Do we know that all code paths in new traversal loops are covered by test? Last time we added traversal (context) it took a few releases to flush the bugs out. Maybe there's some fuzz that can help?

@acdlite acdlite force-pushed the placeholder-children branch from add3f57 to 68f9ccc Compare October 15, 2018 18:50
Originally I did this to free the `stateNode` field to store a second
set of children. I don't we'll need this anymore, since we use fragment
fibers instead. But I still think using `updateQueue` makes more sense
so I'll leave this in.
If the children timeout, we switch to showing the fallback children in
place of the "primary" children. However, we don't want to delete the
primary children because then their state will be lost (both the React
state and the host state, e.g. uncontrolled form inputs). Instead we
keep them mounted and hide them. Both the fallback children AND the
primary children are rendered at the same time. Once the primary
children are un-suspended, we can delete the fallback children — don't
need to preserve their state.

The two sets of children are siblings in the host environment, but
semantically, for purposes of reconciliation, they are two separate
sets. So we store them using two fragment fibers.

However, we want to avoid allocating extra fibers for every placeholder.
They're only necessary when the children time out, because that's the
only time when both sets are mounted.

So, the extra fragment fibers are only used if the children time out.
Otherwise, we render the primary children directly. This requires some
custom reconciliation logic to preserve the state of the primary
children. It's essentially a very basic form of re-parenting.
@acdlite acdlite force-pushed the placeholder-children branch 2 times, most recently from 9f93c3c to 3079cd2 Compare October 15, 2018 22:10
SuspenseComponent has three pieces of state:

- alreadyCaptured: Whether a component in the child subtree already
suspended. If true, subsequent suspends should bubble up to the
next boundary.
- didTimeout: Whether the boundary renders the primary or fallback
children. This is separate from `alreadyCaptured` because outside of
strict mode, when a boundary times out, the first commit renders the
primary children in an incomplete state, then performs a second commit
to switch the fallback. In that first commit, `alreadyCaptured` is
false and `didTimeout` is true.
- timedOutAt: The time at which the boundary timed out. This is separate
from `didTimeout` because it's not set unless the boundary
actually commits.


These were previously spread across several fields.

This happens to make the non-strict case a bit less hacky; the logic for
that special case is now mostly localized to the UnwindWork module.
@acdlite acdlite force-pushed the placeholder-children branch from 3079cd2 to 0389330 Compare October 16, 2018 02:03
@acdlite acdlite force-pushed the placeholder-children branch 3 times, most recently from 95217d7 to 36ce895 Compare October 18, 2018 20:28
This will be used in subsequent commits to test that timed-out children
are properly hidden.

Also adds getChildrenAsJSX() method as an alternative to using
getChildren(). (Ideally all our tests would use test renderer #oneday.)
When a subtree takes too long to load, we swap its contents out for
a fallback to unblock the rest of the tree. Because we don't want
to lose the state of the timed out view, we shouldn't actually delete
the nodes from the tree. Instead, we'll keep them mounted and hide
them visually. When the subtree is unblocked, we un-hide it, having
preserved the existing state.

Adds additional host config methods. For mutation mode:

- hideInstance
- hideTextInstance
- unhideInstance
- unhideTextInstance

For persistent mode:

- cloneHiddenInstance
- cloneUnhiddenInstance
- createHiddenTextInstance

I've only implemented the new methods in the noop and test renderers.
I'll implement them in the other renderers in subsequent commits.
For DOM nodes, we hide using `el.style.display = 'none'`.

Text nodes don't have style, so we hide using `text.textContent = ''`.
Need to distinguish mount from update. An unfortunate edge case :(
In non-concurrent mode, indeterminate fibers may commit in an
inconsistent state. But when they update, we should throw out the
old fiber and start fresh. Which means the new fiber needs a
placement effect.
@acdlite acdlite force-pushed the placeholder-children branch from 36ce895 to 62a3dbe Compare October 18, 2018 20:32
// effects on the children. This is super weird but reconcileChildren is
// deeply nested and it doesn't seem worth it to refactor for this edge
// case. (There are unit tests to prevent regressions.)
current = null;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of assigning this, we should just pass null in all places below. So the compiler knows.

@acdlite acdlite merged commit dac9202 into facebook:master Oct 18, 2018

await advanceTimers(1000);

expect(container.textContent).toEqual('ABC');
Copy link
Contributor

@NE-SmallTown NE-SmallTown Oct 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@acdlite Should we add expect for 0 < time < 500 and 500 < time < 1000 just like Suspense-test does?

Copy link
Contributor

@NE-SmallTown NE-SmallTown Oct 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I'm a little confused here, if you could tell me I would be very happy!

Why textContent is Loading... after execute ReactDOM.render immediately? IMO, it should shows Loading... only after time >= 500 just like how Suspense works, doesn't it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is not running in concurrent mode, so placeholders are shown immediately (sync).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bvaughn Thanks, I don't notice that. But if so, seems we don't need to set the maxDuration prop?

);
if (_current !== null) {
// An indeterminate component only mounts if it suspended inside a non-
// concurrent tree, in an inconsistent state. We want to tree it like
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to tree it

🌲

linjiajian999 pushed a commit to linjiajian999/react that referenced this pull request Oct 22, 2018
…eserved (facebook#13823)

* Store the start time on `updateQueue` instead of `stateNode`

Originally I did this to free the `stateNode` field to store a second
set of children. I don't we'll need this anymore, since we use fragment
fibers instead. But I still think using `updateQueue` makes more sense
so I'll leave this in.

* Use fragment fibers to keep the primary and fallback children separate

If the children timeout, we switch to showing the fallback children in
place of the "primary" children. However, we don't want to delete the
primary children because then their state will be lost (both the React
state and the host state, e.g. uncontrolled form inputs). Instead we
keep them mounted and hide them. Both the fallback children AND the
primary children are rendered at the same time. Once the primary
children are un-suspended, we can delete the fallback children — don't
need to preserve their state.

The two sets of children are siblings in the host environment, but
semantically, for purposes of reconciliation, they are two separate
sets. So we store them using two fragment fibers.

However, we want to avoid allocating extra fibers for every placeholder.
They're only necessary when the children time out, because that's the
only time when both sets are mounted.

So, the extra fragment fibers are only used if the children time out.
Otherwise, we render the primary children directly. This requires some
custom reconciliation logic to preserve the state of the primary
children. It's essentially a very basic form of re-parenting.

* Use `memoizedState` to store various pieces of SuspenseComponent's state

SuspenseComponent has three pieces of state:

- alreadyCaptured: Whether a component in the child subtree already
suspended. If true, subsequent suspends should bubble up to the
next boundary.
- didTimeout: Whether the boundary renders the primary or fallback
children. This is separate from `alreadyCaptured` because outside of
strict mode, when a boundary times out, the first commit renders the
primary children in an incomplete state, then performs a second commit
to switch the fallback. In that first commit, `alreadyCaptured` is
false and `didTimeout` is true.
- timedOutAt: The time at which the boundary timed out. This is separate
from `didTimeout` because it's not set unless the boundary
actually commits.


These were previously spread across several fields.

This happens to make the non-strict case a bit less hacky; the logic for
that special case is now mostly localized to the UnwindWork module.

* Hide timed-out Suspense children

When a subtree takes too long to load, we swap its contents out for
a fallback to unblock the rest of the tree. Because we don't want
to lose the state of the timed out view, we shouldn't actually delete
the nodes from the tree. Instead, we'll keep them mounted and hide
them visually. When the subtree is unblocked, we un-hide it, having
preserved the existing state.

Adds additional host config methods. For mutation mode:

- hideInstance
- hideTextInstance
- unhideInstance
- unhideTextInstance

For persistent mode:

- cloneHiddenInstance
- cloneUnhiddenInstance
- createHiddenTextInstance

I've only implemented the new methods in the noop and test renderers.
I'll implement them in the other renderers in subsequent commits.

* Include `hidden` prop in noop renderer's output

This will be used in subsequent commits to test that timed-out children
are properly hidden.

Also adds getChildrenAsJSX() method as an alternative to using
getChildren(). (Ideally all our tests would use test renderer #oneday.)

* Implement hide/unhide host config methods for DOM renderer

For DOM nodes, we hide using `el.style.display = 'none'`.

Text nodes don't have style, so we hide using `text.textContent = ''`.

* Implement hide/unhide host config methods for Art renderer

* Create DOM fixture that tests state preservation of timed out content

* Account for class components that suspend outside concurrent mode

Need to distinguish mount from update. An unfortunate edge case :(

* Fork appendAllChildren between persistent and mutation mode

* Remove redundant check for existence of el.style

* Schedule placement effect on indeterminate components

In non-concurrent mode, indeterminate fibers may commit in an
inconsistent state. But when they update, we should throw out the
old fiber and start fresh. Which means the new fiber needs a
placement effect.

* Pass null instead of current everywhere in mountIndeterminateComponent
acdlite added a commit to acdlite/react that referenced this pull request Nov 8, 2018
Vestigial behavior that should have been removed in facebook#13823.

Found using the Suspense fuzz tester in facebook#14147.
acdlite added a commit to acdlite/react that referenced this pull request Nov 8, 2018
Vestigial behavior that should have been removed in facebook#13823.

Found using the Suspense fuzz tester in facebook#14147.
acdlite added a commit that referenced this pull request Nov 8, 2018
…14157)

Vestigial behavior that should have been removed in #13823.

Found using the Suspense fuzz tester in #14147.
jetoneza pushed a commit to jetoneza/react that referenced this pull request Jan 23, 2019
…eserved (facebook#13823)

* Store the start time on `updateQueue` instead of `stateNode`

Originally I did this to free the `stateNode` field to store a second
set of children. I don't we'll need this anymore, since we use fragment
fibers instead. But I still think using `updateQueue` makes more sense
so I'll leave this in.

* Use fragment fibers to keep the primary and fallback children separate

If the children timeout, we switch to showing the fallback children in
place of the "primary" children. However, we don't want to delete the
primary children because then their state will be lost (both the React
state and the host state, e.g. uncontrolled form inputs). Instead we
keep them mounted and hide them. Both the fallback children AND the
primary children are rendered at the same time. Once the primary
children are un-suspended, we can delete the fallback children — don't
need to preserve their state.

The two sets of children are siblings in the host environment, but
semantically, for purposes of reconciliation, they are two separate
sets. So we store them using two fragment fibers.

However, we want to avoid allocating extra fibers for every placeholder.
They're only necessary when the children time out, because that's the
only time when both sets are mounted.

So, the extra fragment fibers are only used if the children time out.
Otherwise, we render the primary children directly. This requires some
custom reconciliation logic to preserve the state of the primary
children. It's essentially a very basic form of re-parenting.

* Use `memoizedState` to store various pieces of SuspenseComponent's state

SuspenseComponent has three pieces of state:

- alreadyCaptured: Whether a component in the child subtree already
suspended. If true, subsequent suspends should bubble up to the
next boundary.
- didTimeout: Whether the boundary renders the primary or fallback
children. This is separate from `alreadyCaptured` because outside of
strict mode, when a boundary times out, the first commit renders the
primary children in an incomplete state, then performs a second commit
to switch the fallback. In that first commit, `alreadyCaptured` is
false and `didTimeout` is true.
- timedOutAt: The time at which the boundary timed out. This is separate
from `didTimeout` because it's not set unless the boundary
actually commits.


These were previously spread across several fields.

This happens to make the non-strict case a bit less hacky; the logic for
that special case is now mostly localized to the UnwindWork module.

* Hide timed-out Suspense children

When a subtree takes too long to load, we swap its contents out for
a fallback to unblock the rest of the tree. Because we don't want
to lose the state of the timed out view, we shouldn't actually delete
the nodes from the tree. Instead, we'll keep them mounted and hide
them visually. When the subtree is unblocked, we un-hide it, having
preserved the existing state.

Adds additional host config methods. For mutation mode:

- hideInstance
- hideTextInstance
- unhideInstance
- unhideTextInstance

For persistent mode:

- cloneHiddenInstance
- cloneUnhiddenInstance
- createHiddenTextInstance

I've only implemented the new methods in the noop and test renderers.
I'll implement them in the other renderers in subsequent commits.

* Include `hidden` prop in noop renderer's output

This will be used in subsequent commits to test that timed-out children
are properly hidden.

Also adds getChildrenAsJSX() method as an alternative to using
getChildren(). (Ideally all our tests would use test renderer #oneday.)

* Implement hide/unhide host config methods for DOM renderer

For DOM nodes, we hide using `el.style.display = 'none'`.

Text nodes don't have style, so we hide using `text.textContent = ''`.

* Implement hide/unhide host config methods for Art renderer

* Create DOM fixture that tests state preservation of timed out content

* Account for class components that suspend outside concurrent mode

Need to distinguish mount from update. An unfortunate edge case :(

* Fork appendAllChildren between persistent and mutation mode

* Remove redundant check for existence of el.style

* Schedule placement effect on indeterminate components

In non-concurrent mode, indeterminate fibers may commit in an
inconsistent state. But when they update, we should throw out the
old fiber and start fresh. Which means the new fiber needs a
placement effect.

* Pass null instead of current everywhere in mountIndeterminateComponent
jetoneza pushed a commit to jetoneza/react that referenced this pull request Jan 23, 2019
…acebook#14157)

Vestigial behavior that should have been removed in facebook#13823.

Found using the Suspense fuzz tester in facebook#14147.
n8schloss pushed a commit to n8schloss/react that referenced this pull request Jan 31, 2019
…acebook#14157)

Vestigial behavior that should have been removed in facebook#13823.

Found using the Suspense fuzz tester in facebook#14147.
NMinhNguyen referenced this pull request in enzymejs/react-shallow-renderer Jan 29, 2020
…eserved (#13823)

* Store the start time on `updateQueue` instead of `stateNode`

Originally I did this to free the `stateNode` field to store a second
set of children. I don't we'll need this anymore, since we use fragment
fibers instead. But I still think using `updateQueue` makes more sense
so I'll leave this in.

* Use fragment fibers to keep the primary and fallback children separate

If the children timeout, we switch to showing the fallback children in
place of the "primary" children. However, we don't want to delete the
primary children because then their state will be lost (both the React
state and the host state, e.g. uncontrolled form inputs). Instead we
keep them mounted and hide them. Both the fallback children AND the
primary children are rendered at the same time. Once the primary
children are un-suspended, we can delete the fallback children — don't
need to preserve their state.

The two sets of children are siblings in the host environment, but
semantically, for purposes of reconciliation, they are two separate
sets. So we store them using two fragment fibers.

However, we want to avoid allocating extra fibers for every placeholder.
They're only necessary when the children time out, because that's the
only time when both sets are mounted.

So, the extra fragment fibers are only used if the children time out.
Otherwise, we render the primary children directly. This requires some
custom reconciliation logic to preserve the state of the primary
children. It's essentially a very basic form of re-parenting.

* Use `memoizedState` to store various pieces of SuspenseComponent's state

SuspenseComponent has three pieces of state:

- alreadyCaptured: Whether a component in the child subtree already
suspended. If true, subsequent suspends should bubble up to the
next boundary.
- didTimeout: Whether the boundary renders the primary or fallback
children. This is separate from `alreadyCaptured` because outside of
strict mode, when a boundary times out, the first commit renders the
primary children in an incomplete state, then performs a second commit
to switch the fallback. In that first commit, `alreadyCaptured` is
false and `didTimeout` is true.
- timedOutAt: The time at which the boundary timed out. This is separate
from `didTimeout` because it's not set unless the boundary
actually commits.


These were previously spread across several fields.

This happens to make the non-strict case a bit less hacky; the logic for
that special case is now mostly localized to the UnwindWork module.

* Hide timed-out Suspense children

When a subtree takes too long to load, we swap its contents out for
a fallback to unblock the rest of the tree. Because we don't want
to lose the state of the timed out view, we shouldn't actually delete
the nodes from the tree. Instead, we'll keep them mounted and hide
them visually. When the subtree is unblocked, we un-hide it, having
preserved the existing state.

Adds additional host config methods. For mutation mode:

- hideInstance
- hideTextInstance
- unhideInstance
- unhideTextInstance

For persistent mode:

- cloneHiddenInstance
- cloneUnhiddenInstance
- createHiddenTextInstance

I've only implemented the new methods in the noop and test renderers.
I'll implement them in the other renderers in subsequent commits.

* Include `hidden` prop in noop renderer's output

This will be used in subsequent commits to test that timed-out children
are properly hidden.

Also adds getChildrenAsJSX() method as an alternative to using
getChildren(). (Ideally all our tests would use test renderer #oneday.)

* Implement hide/unhide host config methods for DOM renderer

For DOM nodes, we hide using `el.style.display = 'none'`.

Text nodes don't have style, so we hide using `text.textContent = ''`.

* Implement hide/unhide host config methods for Art renderer

* Create DOM fixture that tests state preservation of timed out content

* Account for class components that suspend outside concurrent mode

Need to distinguish mount from update. An unfortunate edge case :(

* Fork appendAllChildren between persistent and mutation mode

* Remove redundant check for existence of el.style

* Schedule placement effect on indeterminate components

In non-concurrent mode, indeterminate fibers may commit in an
inconsistent state. But when they update, we should throw out the
old fiber and start fresh. Which means the new fiber needs a
placement effect.

* Pass null instead of current everywhere in mountIndeterminateComponent
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants