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
Prev Previous commit
Next Next commit
Refactor isFiberMountedImpl to getNearestMountedFiber
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.
  • Loading branch information
sebmarkbage committed Sep 5, 2019
commit fbaca52bf4a173747ac0b2ce0652f34008cc3e64
29 changes: 14 additions & 15 deletions packages/react-dom/src/events/ReactDOMEventListener.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
} from 'legacy-events/ReactGenericBatching';
import {runExtractedPluginEventsInBatch} from 'legacy-events/EventPluginHub';
import {dispatchEventForResponderEventSystem} from '../events/DOMEventResponderSystem';
import {isFiberMounted} from 'react-reconciler/reflection';
import {getNearestMountedFiber} from 'react-reconciler/reflection';
import {
HostRoot,
SuspenseComponent,
Expand Down Expand Up @@ -322,32 +322,31 @@ export function dispatchEvent(
let targetInst = getClosestInstanceFromNode(nativeEventTarget);

if (targetInst !== null) {
if (isFiberMounted(targetInst)) {
if (targetInst.tag === SuspenseComponent) {
let nearestMounted = getNearestMountedFiber(targetInst);
if (nearestMounted === null) {
// This tree has been unmounted already.
targetInst = null;
} else {
if (nearestMounted.tag === SuspenseComponent) {
sebmarkbage marked this conversation as resolved.
Show resolved Hide resolved
// TODO: This is a good opportunity to schedule a replay of
// the event instead once this boundary has been hydrated.
// For now we're going to just ignore this event as if it's
// not mounted.
targetInst = null;
} else if (targetInst.tag === HostRoot) {
} else if (nearestMounted.tag === HostRoot) {
// We have not yet mounted/hydrated the first children.
// TODO: This is a good opportunity to schedule a replay of
// the event instead once this root has been hydrated.
// For now we're going to just ignore this event as if it's
// not mounted.
targetInst = null;
} else if (nearestMounted !== targetInst) {
// If we get an event (ex: img onload) before committing that
// component's mount, ignore it for now (that is, treat it as if it was an
// event on a non-React tree). We might also consider queueing events and
// dispatching them after the mount.
targetInst = null;
}
} else {
// TODO: If the nearest match was not mounted because it's part
// an in-progress hydration, this would be a good time schedule
// a replay of the event. However, we don't have easy access to
// the HostRoot or SuspenseComponent here.

// If we get an event (ex: img onload) before committing that
// component's mount, ignore it for now (that is, treat it as if it was an
// event on a non-React tree). We might also consider queueing events and
// dispatching them after the mount.
targetInst = null;
}
}

Expand Down
23 changes: 14 additions & 9 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -947,20 +947,25 @@ function updateHostRoot(current, workInProgress, renderExpirationTime) {
// be any children to hydrate which is effectively the same thing as
// not hydrating.

// Mark the host root with a Hydrating effect to know that we're
// currently in a mounting state. That way isMounted, findDOMNode and
// event replaying works as expected.
workInProgress.effectTag |= Hydrating;

// Ensure that children mount into this root without tracking
// side-effects. This ensures that we don't store Placement effects on
// nodes that will be hydrated.
workInProgress.child = mountChildFibers(
let child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderExpirationTime,
);
workInProgress.child = child;

let node = child;
while (node) {
// Mark each child as hydrating. This is a fast path to know whether this
// tree is part of a hydrating tree. This is used to determine if a child
// node has fully mounted yet, and for scheduling event replaying.
// Conceptually this is similar to Placement in that a new subtree is
// inserted into the React tree here. It just happens to not need DOM
// mutations because it already exists.
node.effectTag = (node.effectTag & ~Placement) | Hydrating;
node = node.sibling;
}
} else {
// Otherwise reset hydration state in case we aborted and resumed another
// root.
Expand Down
4 changes: 2 additions & 2 deletions packages/react-reconciler/src/ReactFiberHydrationContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
HostRoot,
SuspenseComponent,
} from 'shared/ReactWorkTags';
import {Deletion, Placement} from 'shared/ReactSideEffectTags';
import {Deletion, Placement, Hydrating} from 'shared/ReactSideEffectTags';
import invariant from 'shared/invariant';

import {
Expand Down Expand Up @@ -140,7 +140,7 @@ function deleteHydratableInstance(
}

function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
fiber.effectTag |= Placement;
fiber.effectTag = (fiber.effectTag & ~Hydrating) | Placement;
if (__DEV__) {
switch (returnFiber.tag) {
case HostRoot: {
Expand Down
30 changes: 14 additions & 16 deletions packages/react-reconciler/src/ReactFiberTreeReflection.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,20 @@ import {enableFundamentalAPI} from 'shared/ReactFeatureFlags';

const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;

const MOUNTING = 1;
const MOUNTED = 2;
const UNMOUNTED = 3;

type MountState = 1 | 2 | 3;

function isFiberMountedImpl(fiber: Fiber): MountState {
export function getNearestMountedFiber(fiber: Fiber): null | Fiber {
let node = fiber;
let nearestMounted = fiber;
if (!fiber.alternate) {
// If there is no alternate, this might be a new tree that isn't inserted
// yet. If it is, then it will have a pending insertion effect on it.
let nextNode = node;
do {
node = nextNode;
if ((node.effectTag & (Placement | Hydrating)) !== NoEffect) {
return MOUNTING;
// This is an insertion or in-progress hydration. The nearest possible
// mounted fiber is the parent but we need to continue to figure out
// if that one is still mounted.
nearestMounted = node.return;
}
nextNode = node.return;
} while (nextNode);
Expand All @@ -55,15 +53,15 @@ function isFiberMountedImpl(fiber: Fiber): MountState {
if (node.tag === HostRoot) {
// TODO: Check if this was a nested HostRoot when used with
// renderContainerIntoSubtree.
return MOUNTED;
return nearestMounted;
}
// If we didn't hit the root, that means that we're in an disconnected tree
// that has been unmounted.
return UNMOUNTED;
return null;
}

export function isFiberMounted(fiber: Fiber): boolean {
return isFiberMountedImpl(fiber) === MOUNTED;
return getNearestMountedFiber(fiber) === fiber;
}

export function isMounted(component: React$Component<any, any>): boolean {
Expand All @@ -89,12 +87,12 @@ export function isMounted(component: React$Component<any, any>): boolean {
if (!fiber) {
return false;
}
return isFiberMountedImpl(fiber) === MOUNTED;
return getNearestMountedFiber(fiber) === fiber;
}

function assertIsMounted(fiber) {
invariant(
isFiberMountedImpl(fiber) === MOUNTED,
getNearestMountedFiber(fiber) === fiber,
'Unable to find node on an unmounted component.',
);
}
Expand All @@ -103,12 +101,12 @@ export function findCurrentFiberUsingSlowPath(fiber: Fiber): Fiber | null {
let alternate = fiber.alternate;
if (!alternate) {
// If there is no alternate, then we only need to check if it is mounted.
const state = isFiberMountedImpl(fiber);
const nearestMounted = getNearestMountedFiber(fiber);
invariant(
state !== UNMOUNTED,
nearestMounted !== null,
'Unable to find node on an unmounted component.',
);
if (state === MOUNTING) {
if (nearestMounted !== fiber) {
return null;
}
return fiber;
Expand Down