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

[Partial Hydration] Don't invoke listeners on parent of dehydrated event target #16591

Merged
merged 8 commits into from
Sep 5, 2019

Conversation

sebmarkbage
Copy link
Collaborator

@sebmarkbage sebmarkbage commented Aug 28, 2019

If an event gets invoked on a child of a suspense boundary we haven't hydrated yet, that's an opportunity where we might want to consider replaying it instead. In the existing semantics there exist no replaying, instead things are ignored (see #16532). So this PR makes sure we don't dispatch these events to React's event system. They may still have been invoked on non-React nodes.

However the tricky part is that we don't readily know if a node is a non-React DOM node or if it's just a node we haven't gotten to yet. We can't just mark the node as dehydrated since the server streaming can update the content of dehydrated boundaries as they go and that would lose the markers.

However, typically there will at least be some React DOM node that is a parent of the Suspense boundary so that will normally become the target today. We also need to deal with the same case when there is a Suspense boundary at the root and while the root most level is concurrently hydrating.

In the case where there is a parent React DOM node, we don't know if the target node was a non-React DOM node that someone manually inserted or if it is a child of a dehydrated boundary.

The common case is that if it's a DOM node that someone messes with, it won't have any children so we can use that as a quick bailout to assume it's not a React node.

If it does have children, I backtrack on the previous siblings to see if we're nested inside a Suspense boundary (i.e. if we're inside two comment nodes). This could potentially be expensive if there are many previous siblings but most of the time there's only one direct DOM node inside a Suspense boundary, and it's unusual that it wouldn't be a Suspense boundary in this case. The worst case is that this is happening on a non-React node and that the React parent happens to have a child that renders null or something in it, and also that this has many children in it.

@sizebot
Copy link

sizebot commented Aug 28, 2019

ReactDOM: size: 0.0%, gzip: -0.0%

Details of bundled changes.

Comparing: 0da7bd0...e50388e

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.profiling.min.js +0.3% +0.4% 115.25 KB 115.65 KB 36.33 KB 36.48 KB NODE_PROFILING
ReactDOM-dev.js +0.5% +0.4% 933.83 KB 938.89 KB 206.71 KB 207.55 KB FB_WWW_DEV
react-dom-server.browser.production.min.js 0.0% -0.0% 19.74 KB 19.74 KB 7.34 KB 7.34 KB UMD_PROD
react-dom-unstable-fizz.browser.development.js 0.0% +0.1% 3.78 KB 3.78 KB 1.53 KB 1.53 KB UMD_DEV
react-dom-test-utils.production.min.js 0.0% 0.0% 11.18 KB 11.18 KB 4.15 KB 4.15 KB UMD_PROD
react-dom-unstable-fizz.browser.production.min.js 0.0% 🔺+0.3% 1.2 KB 1.2 KB 699 B 701 B UMD_PROD
react-dom-unstable-fizz.browser.development.js 0.0% +0.1% 3.61 KB 3.61 KB 1.48 KB 1.48 KB NODE_DEV
react-dom-test-utils.production.min.js 0.0% -0.0% 10.95 KB 10.95 KB 4.09 KB 4.09 KB NODE_PROD
react-dom-unstable-fizz.browser.production.min.js 0.0% 🔺+0.2% 1.04 KB 1.04 KB 631 B 632 B NODE_PROD
react-dom.development.js +0.5% +0.2% 909.72 KB 914.67 KB 206.59 KB 206.96 KB UMD_DEV
react-dom.production.min.js 🔺+0.4% 🔺+0.5% 111.64 KB 112.05 KB 36.01 KB 36.2 KB UMD_PROD
react-dom.profiling.min.js +0.4% +0.4% 115.05 KB 115.46 KB 37.05 KB 37.19 KB UMD_PROFILING
react-dom.development.js +0.5% +0.2% 904.01 KB 908.97 KB 204.98 KB 205.41 KB NODE_DEV
react-dom.production.min.js 🔺+0.4% 🔺+0.4% 111.61 KB 112.01 KB 35.42 KB 35.57 KB NODE_PROD
ReactDOM-prod.js 🔺+0.7% 🔺+0.4% 369.71 KB 372.19 KB 67.8 KB 68.08 KB FB_WWW_PROD
ReactDOM-profiling.js +0.7% +0.4% 374.44 KB 376.91 KB 68.85 KB 69.13 KB FB_WWW_PROFILING
react-dom-unstable-native-dependencies.production.min.js 0.0% 0.0% 10.75 KB 10.75 KB 3.67 KB 3.68 KB UMD_PROD
ReactDOMServer-prod.js 0.0% -0.0% 48.13 KB 48.13 KB 11.05 KB 11.05 KB FB_WWW_PROD
react-dom-unstable-native-dependencies.production.min.js 0.0% 0.0% 10.49 KB 10.49 KB 3.58 KB 3.58 KB NODE_PROD

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.development.js +0.1% 0.0% 650.62 KB 651.45 KB 142.02 KB 142.07 KB UMD_DEV
react-art.production.min.js 0.0% 0.0% 101.86 KB 101.86 KB 31.13 KB 31.14 KB UMD_PROD
react-art.development.js +0.1% 0.0% 581.49 KB 582.32 KB 124.62 KB 124.67 KB NODE_DEV
ReactART-dev.js +0.2% +0.1% 596.5 KB 597.42 KB 124.34 KB 124.41 KB FB_WWW_DEV
ReactART-prod.js 0.0% 0.0% 221.98 KB 222.07 KB 37.77 KB 37.78 KB FB_WWW_PROD

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactTestRenderer-dev.js +0.2% +0.1% 607.58 KB 608.5 KB 126.76 KB 126.83 KB FB_WWW_DEV
react-test-renderer-shallow.production.min.js 0.0% 0.0% 11.41 KB 11.41 KB 3.53 KB 3.53 KB UMD_PROD
react-test-renderer-shallow.production.min.js 0.0% 0.0% 11.55 KB 11.55 KB 3.62 KB 3.62 KB NODE_PROD
react-test-renderer.development.js +0.1% 0.0% 594.65 KB 595.48 KB 127.33 KB 127.38 KB UMD_DEV
react-test-renderer.production.min.js 0.0% 0.0% 68.81 KB 68.81 KB 21.16 KB 21.16 KB UMD_PROD
react-test-renderer.development.js +0.1% 0.0% 590.19 KB 591.01 KB 126.19 KB 126.24 KB NODE_DEV

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler.development.js +0.1% +0.1% 580.13 KB 580.99 KB 123.29 KB 123.36 KB NODE_DEV
react-reconciler.production.min.js 0.0% 0.0% 68.87 KB 68.87 KB 20.42 KB 20.43 KB NODE_PROD
react-reconciler-reflection.development.js 0.0% -0.0% 19.21 KB 19.21 KB 6.32 KB 6.32 KB NODE_DEV
react-reconciler-reflection.production.min.js 0.0% -0.2% 2.56 KB 2.56 KB 1.14 KB 1.14 KB NODE_PROD
react-reconciler-persistent.development.js +0.1% 0.0% 577.17 KB 578.03 KB 122.05 KB 122.1 KB NODE_DEV

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactFabric-dev.js +0.1% 0.0% 735.71 KB 736.57 KB 155.84 KB 155.9 KB RN_FB_DEV
ReactNativeRenderer-dev.js +0.1% 0.0% 729.16 KB 730.02 KB 154.68 KB 154.74 KB RN_OSS_DEV
ReactNativeRenderer-dev.js +0.1% 0.0% 729.32 KB 730.18 KB 154.76 KB 154.82 KB RN_FB_DEV
ReactFabric-dev.js +0.1% 0.0% 735.54 KB 736.41 KB 155.77 KB 155.83 KB RN_OSS_DEV

Generated by 🚫 dangerJS

@sebmarkbage
Copy link
Collaborator Author

sebmarkbage commented Aug 28, 2019

We need to be able to get to the Fiber from the suspense boundary to be able to schedule a hydration of it. So I also added a "hydrateSuspenseInstance" call to attach an expando that points back to the Suspense fiber instance like we do for other event targets and for DevTools.

I moved the logic into getInstanceFromNode so that it now returns either a HostComponent, HostTextComponent or SuspenseComponent. The later is only in the case where the target node is inside a dehydrated suspense tree.

We then check if the tag of the returned fiber is a SuspenseComponent. If so, we know to ignore dispatching the event.

@bvaughn This will affect DevTools since inspecting a node can now return a SuspenseComponent as the target of that inspection if it's inspected before the tree has hydrated.

@sebmarkbage
Copy link
Collaborator Author

sebmarkbage commented Aug 28, 2019

I also added an expando on the container node back to the HostRoot fiber. That way we can also detect if an event happens on anything in the root before we've even hydrated anything (e.g. before calling .render() or if the batch/concurrent mode hasn't started rendering the root most DOM nodes yet.

This also lets us detect a dehydrated Suspense boundary that is directly in the root without any additional DOM nodes. Around it.

This also means that getClosestInstanceFromNode now also can return HostRoot fibers along with SuspenseComponent and the existing HostText/HostComponent ones.

EDIT: Looks like this approach doesn't work when the container node is also a React owned DOM node. That's the failing test.

@trueadm
Copy link
Contributor

trueadm commented Aug 28, 2019

If I'm understanding this right, this will re-target an incoming event to be that of a hydrated/active fiber and its nearest DOM node (now with the additional logic of including the root). If this is the case, is this really the behaviour we want? Shouldn't we proceed with building in event capturing and replaying?

In terms of the implementation of what you've built, it looks okay. The failing test likely relates to the quirky way that we handle ancestors with multiple roots. As you've added logic to preacache root fibers on the DOM node, it's over-riding the previously precached non-root fiber that was attached, so it's not able to traverse to the outer component event listeners.

Now getClosestInstanceFromNode can return either a host component,
host text component or suspense component when the suspense
component is dehydrated.

We then use that to ignore events on a suspense component.
This lets us detect if an event happens on this root's subtree before it
has rendered something.
The approach of checking isFiberMounted answers if we might be in an
in-progress hydration but it doesn't answer which root or boundary
might be in-progress so we don't know what to wait for.

This needs some refactoring.
@sizebot
Copy link

sizebot commented Sep 5, 2019

ReactDOM: size: 0.0%, gzip: 0.0%

Details of bundled changes.

Comparing: f705e2b...1616fe3

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.profiling.min.js +0.4% +0.5% 115.32 KB 115.83 KB 36.36 KB 36.55 KB NODE_PROFILING
ReactDOM-dev.js +0.7% +0.7% 936.58 KB 943.56 KB 207.09 KB 208.5 KB FB_WWW_DEV
react-dom-server.browser.production.min.js 0.0% 0.0% 19.8 KB 19.8 KB 7.36 KB 7.36 KB UMD_PROD
react-dom-test-utils.development.js +0.4% +0.3% 57.25 KB 57.46 KB 15.78 KB 15.82 KB UMD_DEV
react-dom-unstable-fizz.browser.development.js 0.0% +0.1% 3.78 KB 3.78 KB 1.53 KB 1.53 KB UMD_DEV
react-dom-test-utils.production.min.js 🔺+0.1% 🔺+0.2% 11.18 KB 11.19 KB 4.15 KB 4.16 KB UMD_PROD
react-dom-test-utils.development.js +0.4% +0.3% 55.52 KB 55.73 KB 15.45 KB 15.49 KB NODE_DEV
react-dom-unstable-fizz.browser.development.js 0.0% +0.1% 3.61 KB 3.61 KB 1.48 KB 1.48 KB NODE_DEV
react-dom-test-utils.production.min.js 🔺+0.1% 🔺+0.1% 10.95 KB 10.96 KB 4.09 KB 4.09 KB NODE_PROD
react-dom.development.js +0.8% +0.5% 912.66 KB 919.52 KB 207.06 KB 208 KB UMD_DEV
react-dom.production.min.js 🔺+0.5% 🔺+0.6% 111.71 KB 112.23 KB 36.03 KB 36.25 KB UMD_PROD
ReactTestUtils-dev.js +0.4% +0.3% 52.73 KB 52.94 KB 14.13 KB 14.18 KB FB_WWW_DEV
react-dom.profiling.min.js +0.5% +0.5% 115.12 KB 115.64 KB 37.07 KB 37.26 KB UMD_PROFILING
react-dom.development.js +0.8% +0.5% 906.75 KB 913.62 KB 205.34 KB 206.38 KB NODE_DEV
react-dom-server.node.development.js 0.0% 0.0% 139.05 KB 139.05 KB 36.34 KB 36.34 KB NODE_DEV
react-dom.production.min.js 🔺+0.5% 🔺+0.6% 111.68 KB 112.2 KB 35.43 KB 35.64 KB NODE_PROD
ReactDOM-prod.js 🔺+0.8% 🔺+0.5% 371.46 KB 374.61 KB 67.98 KB 68.33 KB FB_WWW_PROD
ReactDOM-profiling.js +0.8% +0.5% 376.06 KB 379.2 KB 69.01 KB 69.36 KB FB_WWW_PROFILING
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 60.71 KB 60.71 KB 15.84 KB 15.84 KB UMD_DEV
react-dom-unstable-native-dependencies.production.min.js 0.0% 0.0% 10.75 KB 10.75 KB 3.67 KB 3.68 KB UMD_PROD
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 60.39 KB 60.39 KB 15.72 KB 15.72 KB NODE_DEV
react-dom-unstable-fizz.node.development.js 0.0% +0.1% 3.87 KB 3.87 KB 1.51 KB 1.51 KB NODE_DEV
react-dom-unstable-native-dependencies.production.min.js 0.0% 0.0% 10.49 KB 10.49 KB 3.58 KB 3.58 KB NODE_PROD

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.development.js +0.2% +0.2% 655.77 KB 657.2 KB 142.94 KB 143.16 KB UMD_DEV
react-art.production.min.js 0.0% -0.1% 101.93 KB 101.97 KB 31.16 KB 31.15 KB UMD_PROD
react-art.development.js +0.2% +0.2% 586.44 KB 587.87 KB 125.48 KB 125.7 KB NODE_DEV
react-art.production.min.js 🔺+0.1% 🔺+0.1% 66.94 KB 66.98 KB 20.4 KB 20.41 KB NODE_PROD
ReactART-dev.js +0.3% +0.2% 601.49 KB 603.02 KB 125.19 KB 125.44 KB FB_WWW_DEV
ReactART-prod.js 🔺+0.1% 🔺+0.1% 225.62 KB 225.89 KB 38.36 KB 38.4 KB FB_WWW_PROD

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactTestRenderer-dev.js +0.3% +0.2% 612.59 KB 614.12 KB 127.62 KB 127.86 KB FB_WWW_DEV
react-test-renderer-shallow.production.min.js 0.0% 0.0% 11.55 KB 11.55 KB 3.62 KB 3.62 KB NODE_PROD
react-test-renderer.development.js +0.2% +0.2% 599.8 KB 601.23 KB 128.26 KB 128.48 KB UMD_DEV
react-test-renderer.production.min.js 🔺+0.1% 0.0% 68.88 KB 68.93 KB 21.18 KB 21.19 KB UMD_PROD
react-test-renderer.development.js +0.2% +0.2% 595.13 KB 596.56 KB 127.06 KB 127.28 KB NODE_DEV
react-test-renderer.production.min.js 🔺+0.1% 🔺+0.1% 68.57 KB 68.62 KB 20.9 KB 20.91 KB NODE_PROD

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler.development.js +0.2% +0.2% 585.08 KB 586.54 KB 124.19 KB 124.41 KB NODE_DEV
react-reconciler.production.min.js 🔺+0.1% 🔺+0.1% 68.94 KB 69.02 KB 20.44 KB 20.47 KB NODE_PROD
react-reconciler-reflection.development.js +1.4% +0.8% 19.25 KB 19.52 KB 6.33 KB 6.38 KB NODE_DEV
react-reconciler-reflection.production.min.js 🔺+1.7% 🔺+1.5% 2.56 KB 2.6 KB 1.14 KB 1.16 KB NODE_PROD
react-reconciler-persistent.development.js +0.3% +0.2% 582.09 KB 583.55 KB 122.92 KB 123.16 KB NODE_DEV
react-reconciler-persistent.production.min.js 🔺+0.1% 🔺+0.1% 68.95 KB 69.03 KB 20.45 KB 20.47 KB NODE_PROD

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactNativeRenderer-prod.js 🔺+0.1% 🔺+0.1% 269.99 KB 270.18 KB 46.28 KB 46.31 KB RN_OSS_PROD
ReactNativeRenderer-profiling.js +0.1% +0.1% 278.4 KB 278.59 KB 47.8 KB 47.84 KB RN_OSS_PROFILING
ReactFabric-prod.js 🔺+0.1% 🔺+0.1% 261.69 KB 261.88 KB 44.95 KB 44.98 KB RN_OSS_PROD
ReactFabric-profiling.js +0.1% +0.1% 270.44 KB 270.63 KB 46.47 KB 46.5 KB RN_OSS_PROFILING
ReactFabric-dev.js +0.2% +0.2% 740.7 KB 742.18 KB 156.69 KB 156.95 KB RN_FB_DEV
ReactFabric-prod.js 🔺+0.1% 🔺+0.1% 261.7 KB 261.89 KB 44.96 KB 44.99 KB RN_FB_PROD
ReactNativeRenderer-dev.js +0.2% +0.2% 734.15 KB 735.63 KB 155.53 KB 155.79 KB RN_OSS_DEV
ReactFabric-profiling.js +0.1% +0.1% 270.44 KB 270.63 KB 46.48 KB 46.51 KB RN_FB_PROFILING
ReactNativeRenderer-dev.js +0.2% +0.2% 734.31 KB 735.79 KB 155.61 KB 155.87 KB RN_FB_DEV
ReactNativeRenderer-prod.js 🔺+0.1% 🔺+0.1% 269.99 KB 270.18 KB 46.29 KB 46.32 KB RN_FB_PROD
ReactNativeRenderer-profiling.js +0.1% +0.1% 278.39 KB 278.58 KB 47.81 KB 47.84 KB RN_FB_PROFILING
ReactFabric-dev.js +0.2% +0.2% 740.53 KB 742.01 KB 156.62 KB 156.87 KB RN_OSS_DEV

Generated by 🚫 dangerJS against 1616fe3

@sebmarkbage
Copy link
Collaborator Author

sebmarkbage commented Sep 5, 2019

I updated the root approach to track a separate expando on the container. That way we can differentiate between a DOM node in its role as a Container and its role as a leaf Instance. Now everything passes.

We'll need the nearest boundary for event replaying so this prepares for
that.

This surfaced an issue that we attach Hydrating tag on the root but normally
this (and Placement) is attached on the child. This surfaced an issue
that this can lead to both Placement and Hydrating effects which is not
supported so we need to ensure that we only ever use one or the other.

export function precacheFiberNode(hostInst, node) {
node[internalInstanceKey] = hostInst;
}

export function markContainerAsRoot(hostRoot, node) {
node[internalContainerInstanceKey] = hostRoot;
Copy link
Contributor

@trueadm trueadm Sep 5, 2019

Choose a reason for hiding this comment

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

I do wonder if its still the best strategy to keep adding additional properties to DOM nodes vs a WeakMap with an object that contains { instance, container, props }. We could probably unify this with the already existing WeakMap we have on DOM nodes for checking for registered events:

https://github.com/facebook/react/blob/master/packages/react-dom/src/events/ReactBrowserEventEmitter.js#L89

This isn't really something for this PR, just an observation.

Update: After looking at this jsperf it might be better if we should stick with a hidden key, but rather than having 3, have a single one and remove the WeakMap and use an object with 4 properties.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The one for props is an unfortunate one. That was never intentional. I just realized too late I didn't have a convenient place to find the current Fiber. I don't remember exactly why I couldn't just update the fiber pointer to the correct alternate every time events change and then read from memoizedProps. There's probably something we can do to get rid of that one.

Regarding the new container one. It's very rare that something will both be a container and an instance so there will almost never be more than one expando. So for that case it's probably not worth adding an indirection for the very common case just to avoid two slots in the rare case.

Copy link
Contributor

@trueadm trueadm left a comment

Choose a reason for hiding this comment

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

Looks good to me.

@gaearon
Copy link
Collaborator

gaearon commented Sep 5, 2019

If I recall correctly we need this expando to fix iOS clicks in portals anyway.

@sebmarkbage
Copy link
Collaborator Author

sebmarkbage commented Sep 5, 2019

@gaearon Note that this doesn't actually add one for portals. Only roots. Mostly because Portals don't support hydration anyway.

The container is inside the instance, so we must find it before the
instance, since otherwise we'll miss it.
@sebmarkbage
Copy link
Collaborator Author

sebmarkbage commented Sep 5, 2019

I added a note that calling getClosestInstanceFromNode on a Container DOM node (or Suspense comment) doesn't actually return the HostRoot (nor SuspenseComponent). That's because conceptually the Fiber doesn't actually represent the Container DOM node. It represents a virtual thing that exists as the children of the Container DOM node. So it's between the Container DOM node and the first DOM node child. Similar to a Fragment. (If we had first-class persistent fragments in the DOM that might be a good way to represent HostRoot and SuspenseComponents instead of comments.)

Therefore, to get to a HostRoot, you have to pass a child of the DOM Container node.

This isn't really all that important nuance but it does allow me to take some short cuts (e.g. for the common case where the target is an instance) and gives me a convenient hint that a node is indeed dehydrated.

@sebmarkbage
Copy link
Collaborator Author

I'll also note for future us, that this has a known problem when the Container is a Comment node. However, that is an unstable feature and currently doesn't work with hydration anyway. So that needs some further consideration (if we ever want to support that use case).

@sebmarkbage sebmarkbage merged commit 8d7c733 into facebook:master Sep 5, 2019
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.

None yet

5 participants