@samitouri / QOS-React-2 / commits / 8d7c733f1f

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

* Don't invoke listeners on parent of dehydrated event target * Move Suspense boundary check to getClosestInstanceFromNode 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. * Attach the HostRoot fiber to the DOM container This lets us detect if an event happens on this root's subtree before it has rendered something. * Add todo 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. * 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. * Add todo for bug I spotted * Cache tags * Check the ContainerInstanceKey before the InstanceKey The container is inside the instance, so we must find it before the instance, since otherwise we'll miss it.

Sebastian Markbåge committed Sep 5, 2019 at 08:51 UTC 8d7c733f1fdad55d0f10947931b378edc5e039ad
14 files changed +444 -65
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+162
@@ -1895,4 +1895,166 @@ describe('ReactDOMServerPartialHydration', () => {
1895
1896 document.body.removeChild(container);
1897 });
1898 +
1899 + it('does not invoke the parent of dehydrated boundary event', async () => {
1900 + let suspend = false;
1901 + let resolve;
1902 + let promise = new Promise(resolvePromise => (resolve = resolvePromise));
1903 +
1904 + let clicksOnParent = 0;
1905 + let clicksOnChild = 0;
1906 +
1907 + function Child({text}) {
1908 + if (suspend) {
1909 + throw promise;
1910 + } else {
1911 + return (
1912 + <span
1913 + onClick={e => {
1914 + // The stopPropagation is showing an example why invoking
1915 + // the event on only a parent might not be correct.
1916 + e.stopPropagation();
1917 + clicksOnChild++;
1918 + }}>
1919 + Hello
1920 + </span>
1921 + );
1922 + }
1923 + }
1924 +
1925 + function App() {
1926 + return (
1927 + <div onClick={() => clicksOnParent++}>
1928 + <Suspense fallback="Loading...">
1929 + <Child />
1930 + </Suspense>
1931 + </div>
1932 + );
1933 + }
1934 +
1935 + suspend = false;
1936 + let finalHTML = ReactDOMServer.renderToString(<App />);
1937 + let container = document.createElement('div');
1938 + container.innerHTML = finalHTML;
1939 +
1940 + // We need this to be in the document since we'll dispatch events on it.
1941 + document.body.appendChild(container);
1942 +
1943 + let span = container.getElementsByTagName('span')[0];
1944 +
1945 + // On the client we don't have all data yet but we want to start
1946 + // hydrating anyway.
1947 + suspend = true;
1948 + let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1949 + root.render(<App />);
1950 + Scheduler.unstable_flushAll();
1951 + jest.runAllTimers();
1952 +
1953 + // We're now partially hydrated.
1954 + span.click();
1955 + expect(clicksOnChild).toBe(0);
1956 + expect(clicksOnParent).toBe(0);
1957 +
1958 + // Resolving the promise so that rendering can complete.
1959 + suspend = false;
1960 + resolve();
1961 + await promise;
1962 +
1963 + Scheduler.unstable_flushAll();
1964 + jest.runAllTimers();
1965 +
1966 + // TODO: With selective hydration the event should've been replayed
1967 + // but for now we'll have to issue it again.
1968 + act(() => {
1969 + span.click();
1970 + });
1971 +
1972 + expect(clicksOnChild).toBe(1);
1973 + // This will be zero due to the stopPropagation.
1974 + expect(clicksOnParent).toBe(0);
1975 +
1976 + document.body.removeChild(container);
1977 + });
1978 +
1979 + it('does not invoke an event on a parent tree when a subtree is dehydrated', async () => {
1980 + let suspend = false;
1981 + let resolve;
1982 + let promise = new Promise(resolvePromise => (resolve = resolvePromise));
1983 +
1984 + let clicks = 0;
1985 + let childSlotRef = React.createRef();
1986 +
1987 + function Parent() {
1988 + return <div onClick={() => clicks++} ref={childSlotRef} />;
1989 + }
1990 +
1991 + function Child({text}) {
1992 + if (suspend) {
1993 + throw promise;
1994 + } else {
1995 + return <a>Click me</a>;
1996 + }
1997 + }
1998 +
1999 + function App() {
2000 + // The root is a Suspense boundary.
2001 + return (
2002 + <Suspense fallback="Loading...">
2003 + <Child />
2004 + </Suspense>
2005 + );
2006 + }
2007 +
2008 + suspend = false;
2009 + let finalHTML = ReactDOMServer.renderToString(<App />);
2010 +
2011 + let parentContainer = document.createElement('div');
2012 + let childContainer = document.createElement('div');
2013 +
2014 + // We need this to be in the document since we'll dispatch events on it.
2015 + document.body.appendChild(parentContainer);
2016 +
2017 + // We're going to use a different root as a parent.
2018 + // This lets us detect whether an event goes through React's event system.
2019 + let parentRoot = ReactDOM.unstable_createRoot(parentContainer);
2020 + parentRoot.render(<Parent />);
2021 + Scheduler.unstable_flushAll();
2022 +
2023 + childSlotRef.current.appendChild(childContainer);
2024 +
2025 + childContainer.innerHTML = finalHTML;
2026 +
2027 + let a = childContainer.getElementsByTagName('a')[0];
2028 +
2029 + suspend = true;
2030 +
2031 + // Hydrate asynchronously.
2032 + let root = ReactDOM.unstable_createRoot(childContainer, {hydrate: true});
2033 + root.render(<App />);
2034 + jest.runAllTimers();
2035 + Scheduler.unstable_flushAll();
2036 +
2037 + // The Suspense boundary is not yet hydrated.
2038 + a.click();
2039 + expect(clicks).toBe(0);
2040 +
2041 + // Resolving the promise so that rendering can complete.
2042 + suspend = false;
2043 + resolve();
2044 + await promise;
2045 +
2046 + Scheduler.unstable_flushAll();
2047 + jest.runAllTimers();
2048 +
2049 + // We're now full hydrated.
2050 + // TODO: With selective hydration the event should've been replayed
2051 + // but for now we'll have to issue it again.
2052 + act(() => {
2053 + a.click();
2054 + });
2055 +
2056 + expect(clicks).toBe(1);
2057 +
2058 + document.body.removeChild(parentContainer);
2059 + });
2060 });
packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+58
@@ -586,4 +586,62 @@ describe('ReactDOMServerHydration', () => {
586
587 document.body.removeChild(container);
588 });
589 +
590 + it('does not invoke an event on a parent tree when a subtree is hydrating', () => {
591 + let clicks = 0;
592 + let childSlotRef = React.createRef();
593 +
594 + function Parent() {
595 + return <div onClick={() => clicks++} ref={childSlotRef} />;
596 + }
597 +
598 + function App() {
599 + return (
600 + <div>
601 + <a>Click me</a>
602 + </div>
603 + );
604 + }
605 +
606 + let finalHTML = ReactDOMServer.renderToString(<App />);
607 +
608 + let parentContainer = document.createElement('div');
609 + let childContainer = document.createElement('div');
610 +
611 + // We need this to be in the document since we'll dispatch events on it.
612 + document.body.appendChild(parentContainer);
613 +
614 + // We're going to use a different root as a parent.
615 + // This lets us detect whether an event goes through React's event system.
616 + let parentRoot = ReactDOM.unstable_createRoot(parentContainer);
617 + parentRoot.render(<Parent />);
618 + Scheduler.unstable_flushAll();
619 +
620 + childSlotRef.current.appendChild(childContainer);
621 +
622 + childContainer.innerHTML = finalHTML;
623 +
624 + let a = childContainer.getElementsByTagName('a')[0];
625 +
626 + // Hydrate asynchronously.
627 + let root = ReactDOM.unstable_createRoot(childContainer, {hydrate: true});
628 + root.render(<App />);
629 + // Nothing has rendered so far.
630 +
631 + a.click();
632 + expect(clicks).toBe(0);
633 +
634 + Scheduler.unstable_flushAll();
635 +
636 + // We're now full hydrated.
637 + // TODO: With selective hydration the event should've been replayed
638 + // but for now we'll have to issue it again.
639 + act(() => {
640 + a.click();
641 + });
642 +
643 + expect(clicks).toBe(1);
644 +
645 + document.body.removeChild(parentContainer);
646 + });
647 });
packages/react-dom/src/client/ReactDOM.js
+3
@@ -70,6 +70,7 @@ import {
70 getNodeFromInstance,
71 getFiberCurrentPropsFromNode,
72 getClosestInstanceFromNode,
73 + markContainerAsRoot,
74 } from './ReactDOMComponentTree';
75 import {restoreControlledState} from './ReactDOMComponent';
76 import {dispatchEvent} from '../events/ReactDOMEventListener';
@@ -375,6 +376,7 @@ function ReactSyncRoot(
376 (options != null && options.hydrationOptions) || null;
377 const root = createContainer(container, tag, hydrate, hydrationCallbacks);
378 this._internalRoot = root;
379 + markContainerAsRoot(root.current, container);
380 }
381
382 function ReactRoot(container: DOMContainer, options: void | RootOptions) {
@@ -388,6 +390,7 @@ function ReactRoot(container: DOMContainer, options: void | RootOptions) {
390 hydrationCallbacks,
391 );
392 this._internalRoot = root;
393 + markContainerAsRoot(root.current, container);
394 }
395
396 ReactRoot.prototype.render = ReactSyncRoot.prototype.render = function(
packages/react-dom/src/client/ReactDOMComponentTree.js
+74 -24
@@ -8,43 +8,93 @@
8 import {HostComponent, HostText} from 'shared/ReactWorkTags';
9 import invariant from 'shared/invariant';
10
11 +import {getParentSuspenseInstance} from './ReactDOMHostConfig';
12 +
13 const randomKey = Math.random()
14 .toString(36)
15 .slice(2);
16 const internalInstanceKey = '__reactInternalInstance$' + randomKey;
17 const internalEventHandlersKey = '__reactEventHandlers$' + randomKey;
18 +const internalContainerInstanceKey = '__reactContainere$' + randomKey;
19
20 export function precacheFiberNode(hostInst, node) {
21 node[internalInstanceKey] = hostInst;
22 }
23
21 -/**
22 - * Given a DOM node, return the closest ReactDOMComponent or
23 - * ReactDOMTextComponent instance ancestor.
24 - */
25 -export function getClosestInstanceFromNode(node) {
26 - let inst = node[internalInstanceKey];
27 - if (inst) {
28 - return inst;
29 - }
24 +export function markContainerAsRoot(hostRoot, node) {
25 + node[internalContainerInstanceKey] = hostRoot;
26 +}
27
31 - do {
32 - node = node.parentNode;
33 - if (node) {
34 - inst = node[internalInstanceKey];
35 - } else {
36 - // Top of the tree. This node must not be part of a React tree (or is
37 - // unmounted, potentially).
38 - return null;
28 +// Given a DOM node, return the closest HostComponent or HostText fiber ancestor.
29 +// If the target node is part of a hydrated or not yet rendered subtree, then
30 +// this may also return a SuspenseComponent or HostRoot to indicate that.
31 +// Conceptually the HostRoot fiber is a child of the Container node. So if you
32 +// pass the Container node as the targetNode, you wiill not actually get the
33 +// HostRoot back. To get to the HostRoot, you need to pass a child of it.
34 +// The same thing applies to Suspense boundaries.
35 +export function getClosestInstanceFromNode(targetNode) {
36 + let targetInst = targetNode[internalInstanceKey];
37 + if (targetInst) {
38 + // Don't return HostRoot or SuspenseComponent here.
39 + return targetInst;
40 + }
41 + // If the direct event target isn't a React owned DOM node, we need to look
42 + // to see if one of its parents is a React owned DOM node.
43 + let parentNode = targetNode.parentNode;
44 + while (parentNode) {
45 + // We'll check if this is a container root that could include
46 + // React nodes in the future. We need to check this first because
47 + // if we're a child of a dehydrated container, we need to first
48 + // find that inner container before moving on to finding the parent
49 + // instance. Note that we don't check this field on the targetNode
50 + // itself because the fibers are conceptually between the container
51 + // node and the first child. It isn't surrounding the container node.
52 + targetInst = parentNode[internalContainerInstanceKey];
53 + if (targetInst) {
54 + // If so, we return the HostRoot Fiber.
55 + return targetInst;
56 }
40 - } while (!inst);
57 + targetInst = parentNode[internalInstanceKey];
58 + if (targetInst) {
59 + // Since this wasn't the direct target of the event, we might have
60 + // stepped past dehydrated DOM nodes to get here. However they could
61 + // also have been non-React nodes. We need to answer which one.
62
42 - let tag = inst.tag;
43 - switch (tag) {
44 - case HostComponent:
45 - case HostText:
46 - // In Fiber, this will always be the deepest root.
47 - return inst;
63 + // If we the instance doesn't have any children, then there can't be
64 + // a nested suspense boundary within it. So we can use this as a fast
65 + // bailout. Most of the time, when people add non-React children to
66 + // the tree, it is using a ref to a child-less DOM node.
67 + // We only need to check one of the fibers because if it has ever
68 + // gone from having children to deleting them or vice versa it would
69 + // have deleted the dehydrated boundary nested inside already.
70 + if (targetInst.child !== null) {
71 + // Next we need to figure out if the node that skipped past is
72 + // nested within a dehydrated boundary and if so, which one.
73 + let suspenseInstance = getParentSuspenseInstance(targetNode);
74 + if (suspenseInstance !== null) {
75 + // We found a suspense instance. That means that we haven't
76 + // hydrated it yet. Even though we leave the comments in the
77 + // DOM after hydrating, and there are boundaries in the DOM
78 + // that could already be hydrated, we wouldn't have found them
79 + // through this pass since if the target is hydrated it would
80 + // have had an internalInstanceKey on it.
81 + // Let's get the fiber associated with the SuspenseComponent
82 + // as the deepest instance.
83 + let targetSuspenseInst = suspenseInstance[internalInstanceKey];
84 + if (targetSuspenseInst) {
85 + return targetSuspenseInst;
86 + }
87 + // If we don't find a Fiber on the comment, it might be because
88 + // we haven't gotten to hydrate it yet. That should mean that
89 + // the parent component also hasn't hydrated yet but we can
90 + // just return that since it will bail out on the isMounted
91 + // check.
92 + }
93 + }
94 + return targetInst;
95 + }
96 + targetNode = parentNode;
97 + parentNode = targetNode.parentNode;
98 }
99 return null;
100 }
packages/react-dom/src/client/ReactDOMHostConfig.js
+40
@@ -673,6 +673,13 @@ export function hydrateTextInstance(
673 return diffHydratedText(textInstance, text);
674 }
675
676 +export function hydrateSuspenseInstance(
677 + suspenseInstance: SuspenseInstance,
678 + internalInstanceHandle: Object,
679 +) {
680 + precacheFiberNode(internalInstanceHandle, suspenseInstance);
681 +}
682 +
683 export function getNextHydratableInstanceAfterSuspenseInstance(
684 suspenseInstance: SuspenseInstance,
685 ): null | HydratableInstance {
@@ -704,6 +711,39 @@ export function getNextHydratableInstanceAfterSuspenseInstance(
711 return null;
712 }
713
714 +// Returns the SuspenseInstance if this node is a direct child of a
715 +// SuspenseInstance. I.e. if its previous sibling is a Comment with
716 +// SUSPENSE_x_START_DATA. Otherwise, null.
717 +export function getParentSuspenseInstance(
718 + targetInstance: Instance,
719 +): null | SuspenseInstance {
720 + let node = targetInstance.previousSibling;
721 + // Skip past all nodes within this suspense boundary.
722 + // There might be nested nodes so we need to keep track of how
723 + // deep we are and only break out when we're back on top.
724 + let depth = 0;
725 + while (node) {
726 + if (node.nodeType === COMMENT_NODE) {
727 + let data = ((node: any).data: string);
728 + if (
729 + data === SUSPENSE_START_DATA ||
730 + data === SUSPENSE_FALLBACK_START_DATA ||
731 + data === SUSPENSE_PENDING_START_DATA
732 + ) {
733 + if (depth === 0) {
734 + return ((node: any): SuspenseInstance);
735 + } else {
736 + depth--;
737 + }
738 + } else if (data === SUSPENSE_END_DATA) {
739 + depth++;
740 + }
741 + }
742 + node = node.previousSibling;
743 + }
744 + return null;
745 +}
746 +
747 export function didNotMatchHydratedContainerTextInstance(
748 parentContainer: Container,
749 textInstance: TextInstance,
packages/react-dom/src/events/EnterLeaveEventPlugin.js
+4
@@ -19,6 +19,7 @@ import {
19 getClosestInstanceFromNode,
20 getNodeFromInstance,
21 } from '../client/ReactDOMComponentTree';
22 +import {HostComponent, HostText} from 'shared/ReactWorkTags';
23
24 const eventTypes = {
25 mouseEnter: {
@@ -89,6 +90,9 @@ const EnterLeaveEventPlugin = {
90 from = targetInst;
91 const related = nativeEvent.relatedTarget || nativeEvent.toElement;
92 to = related ? getClosestInstanceFromNode(related) : null;
93 + if (to !== null && to.tag !== HostComponent && to.tag !== HostText) {
94 + to = null;
95 + }
96 } else {
97 // Moving to a node from outside the window.
98 from = null;
packages/react-dom/src/events/ReactDOMEventListener.js
+42 -13
@@ -22,8 +22,13 @@ import {
22 } from 'legacy-events/ReactGenericBatching';
23 import {runExtractedPluginEventsInBatch} from 'legacy-events/EventPluginHub';
24 import {dispatchEventForResponderEventSystem} from '../events/DOMEventResponderSystem';
25 -import {isFiberMounted} from 'react-reconciler/reflection';
26 -import {HostRoot} from 'shared/ReactWorkTags';
25 +import {getNearestMountedFiber} from 'react-reconciler/reflection';
26 +import {
27 + HostRoot,
28 + SuspenseComponent,
29 + HostComponent,
30 + HostText,
31 +} from 'shared/ReactWorkTags';
32 import {
33 type EventSystemFlags,
34 PLUGIN_EVENT_SYSTEM,
@@ -77,6 +82,9 @@ type BookKeepingInstance = {
82 * other). If React trees are not nested, returns null.
83 */
84 function findRootContainerNode(inst) {
85 + if (inst.tag === HostRoot) {
86 + return inst.stateNode.containerInfo;
87 + }
88 // TODO: It may be a good idea to cache this to prevent unnecessary DOM
89 // traversal, but caching is difficult to do correctly without using a
90 // mutation observer to listen for all DOM changes.
@@ -141,7 +149,10 @@ function handleTopLevel(bookKeeping: BookKeepingInstance) {
149 if (!root) {
150 break;
151 }
144 - bookKeeping.ancestors.push(ancestor);
152 + const tag = ancestor.tag;
153 + if (tag === HostComponent || tag === HostText) {
154 + bookKeeping.ancestors.push(ancestor);
155 + }
156 ancestor = getClosestInstanceFromNode(root);
157 } while (ancestor);
158
@@ -311,16 +322,34 @@ export function dispatchEvent(
322 const nativeEventTarget = getEventTarget(nativeEvent);
323 let targetInst = getClosestInstanceFromNode(nativeEventTarget);
324
314 - if (
315 - targetInst !== null &&
316 - typeof targetInst.tag === 'number' &&
317 - !isFiberMounted(targetInst)
318 - ) {
319 - // If we get an event (ex: img onload) before committing that
320 - // component's mount, ignore it for now (that is, treat it as if it was an
321 - // event on a non-React tree). We might also consider queueing events and
322 - // dispatching them after the mount.
323 - targetInst = null;
325 + if (targetInst !== null) {
326 + let nearestMounted = getNearestMountedFiber(targetInst);
327 + if (nearestMounted === null) {
328 + // This tree has been unmounted already.
329 + targetInst = null;
330 + } else {
331 + const tag = nearestMounted.tag;
332 + if (tag === SuspenseComponent) {
333 + // TODO: This is a good opportunity to schedule a replay of
334 + // the event instead once this boundary has been hydrated.
335 + // For now we're going to just ignore this event as if it's
336 + // not mounted.
337 + targetInst = null;
338 + } else if (tag === HostRoot) {
339 + // We have not yet mounted/hydrated the first children.
340 + // TODO: This is a good opportunity to schedule a replay of
341 + // the event instead once this root has been hydrated.
342 + // For now we're going to just ignore this event as if it's
343 + // not mounted.
344 + targetInst = null;
345 + } else if (nearestMounted !== targetInst) {
346 + // If we get an event (ex: img onload) before committing that
347 + // component's mount, ignore it for now (that is, treat it as if it was an
348 + // event on a non-React tree). We might also consider queueing events and
349 + // dispatching them after the mount.
350 + targetInst = null;
351 + }
352 + }
353 }
354
355 if (enableFlareAPI) {
packages/react-reconciler/src/ReactFiberBeginWork.js
+17 -9
@@ -938,6 +938,9 @@ function updateHostRoot(current, workInProgress, renderExpirationTime) {
938 }
939 const root: FiberRoot = workInProgress.stateNode;
940 if (
941 + // TODO: This is a bug because if we render null after having hydrating,
942 + // we'll reenter hydration state at the next update which will then
943 + // trigger hydration warnings.
944 (current === null || current.child === null) &&
945 root.hydrate &&
946 enterHydrationState(workInProgress)
@@ -947,20 +950,25 @@ function updateHostRoot(current, workInProgress, renderExpirationTime) {
950 // be any children to hydrate which is effectively the same thing as
951 // not hydrating.
952
950 - // Mark the host root with a Hydrating effect to know that we're
951 - // currently in a mounting state. That way isMounted, findDOMNode and
952 - // event replaying works as expected.
953 - workInProgress.effectTag |= Hydrating;
954 -
955 - // Ensure that children mount into this root without tracking
956 - // side-effects. This ensures that we don't store Placement effects on
957 - // nodes that will be hydrated.
958 - workInProgress.child = mountChildFibers(
953 + let child = mountChildFibers(
954 workInProgress,
955 null,
956 nextChildren,
957 renderExpirationTime,
958 );
959 + workInProgress.child = child;
960 +
961 + let node = child;
962 + while (node) {
963 + // Mark each child as hydrating. This is a fast path to know whether this
964 + // tree is part of a hydrating tree. This is used to determine if a child
965 + // node has fully mounted yet, and for scheduling event replaying.
966 + // Conceptually this is similar to Placement in that a new subtree is
967 + // inserted into the React tree here. It just happens to not need DOM
968 + // mutations because it already exists.
969 + node.effectTag = (node.effectTag & ~Placement) | Hydrating;
970 + node = node.sibling;
971 + }
972 } else {
973 // Otherwise reset hydration state in case we aborted and resumed another
974 // root.
packages/react-reconciler/src/ReactFiberCompleteWork.js
+2
@@ -107,6 +107,7 @@ import {popProvider} from './ReactFiberNewContext';
107 import {
108 prepareToHydrateHostInstance,
109 prepareToHydrateHostTextInstance,
110 + prepareToHydrateHostSuspenseInstance,
111 popHydrationState,
112 resetHydrationState,
113 } from './ReactFiberHydrationContext';
@@ -828,6 +829,7 @@ function completeWork(
829 'A dehydrated suspense component was completed without a hydrated node. ' +
830 'This is probably a bug in React.',
831 );
832 + prepareToHydrateHostSuspenseInstance(workInProgress);
833 if (enableSchedulerTracing) {
834 markSpawnedWork(Never);
835 }
packages/react-reconciler/src/ReactFiberHydrationContext.js
+24 -2
@@ -24,7 +24,7 @@ import {
24 HostRoot,
25 SuspenseComponent,
26 } from 'shared/ReactWorkTags';
27 -import {Deletion, Placement} from 'shared/ReactSideEffectTags';
27 +import {Deletion, Placement, Hydrating} from 'shared/ReactSideEffectTags';
28 import invariant from 'shared/invariant';
29
30 import {
@@ -41,6 +41,7 @@ import {
41 getFirstHydratableChild,
42 hydrateInstance,
43 hydrateTextInstance,
44 + hydrateSuspenseInstance,
45 getNextHydratableInstanceAfterSuspenseInstance,
46 didNotMatchHydratedContainerTextInstance,
47 didNotMatchHydratedTextInstance,
@@ -139,7 +140,7 @@ function deleteHydratableInstance(
140 }
141
142 function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
142 - fiber.effectTag |= Placement;
143 + fiber.effectTag = (fiber.effectTag & ~Hydrating) | Placement;
144 if (__DEV__) {
145 switch (returnFiber.tag) {
146 case HostRoot: {
@@ -370,6 +371,26 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): boolean {
371 return shouldUpdate;
372 }
373
374 +function prepareToHydrateHostSuspenseInstance(fiber: Fiber): void {
375 + if (!supportsHydration) {
376 + invariant(
377 + false,
378 + 'Expected prepareToHydrateHostSuspenseInstance() to never be called. ' +
379 + 'This error is likely caused by a bug in React. Please file an issue.',
380 + );
381 + }
382 +
383 + let suspenseState: null | SuspenseState = fiber.memoizedState;
384 + let suspenseInstance: null | SuspenseInstance =
385 + suspenseState !== null ? suspenseState.dehydrated : null;
386 + invariant(
387 + suspenseInstance,
388 + 'Expected to have a hydrated suspense instance. ' +
389 + 'This error is likely caused by a bug in React. Please file an issue.',
390 + );
391 + hydrateSuspenseInstance(suspenseInstance, fiber);
392 +}
393 +
394 function skipPastDehydratedSuspenseInstance(
395 fiber: Fiber,
396 ): null | HydratableInstance {
@@ -471,5 +492,6 @@ export {
492 tryToClaimNextHydratableInstance,
493 prepareToHydrateHostInstance,
494 prepareToHydrateHostTextInstance,
495 + prepareToHydrateHostSuspenseInstance,
496 popHydrationState,
497 };
packages/react-reconciler/src/ReactFiberTreeReflection.js
+14 -16
@@ -28,14 +28,9 @@ import {enableFundamentalAPI} from 'shared/ReactFeatureFlags';
28
29 const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
30
31 -const MOUNTING = 1;
32 -const MOUNTED = 2;
33 -const UNMOUNTED = 3;
34 -
35 -type MountState = 1 | 2 | 3;
36 -
37 -function isFiberMountedImpl(fiber: Fiber): MountState {
31 +export function getNearestMountedFiber(fiber: Fiber): null | Fiber {
32 let node = fiber;
33 + let nearestMounted = fiber;
34 if (!fiber.alternate) {
35 // If there is no alternate, this might be a new tree that isn't inserted
36 // yet. If it is, then it will have a pending insertion effect on it.
@@ -43,7 +38,10 @@ function isFiberMountedImpl(fiber: Fiber): MountState {
38 do {
39 node = nextNode;
40 if ((node.effectTag & (Placement | Hydrating)) !== NoEffect) {
46 - return MOUNTING;
41 + // This is an insertion or in-progress hydration. The nearest possible
42 + // mounted fiber is the parent but we need to continue to figure out
43 + // if that one is still mounted.
44 + nearestMounted = node.return;
45 }
46 nextNode = node.return;
47 } while (nextNode);
@@ -55,15 +53,15 @@ function isFiberMountedImpl(fiber: Fiber): MountState {
53 if (node.tag === HostRoot) {
54 // TODO: Check if this was a nested HostRoot when used with
55 // renderContainerIntoSubtree.
58 - return MOUNTED;
56 + return nearestMounted;
57 }
58 // If we didn't hit the root, that means that we're in an disconnected tree
59 // that has been unmounted.
62 - return UNMOUNTED;
60 + return null;
61 }
62
63 export function isFiberMounted(fiber: Fiber): boolean {
66 - return isFiberMountedImpl(fiber) === MOUNTED;
64 + return getNearestMountedFiber(fiber) === fiber;
65 }
66
67 export function isMounted(component: React$Component<any, any>): boolean {
@@ -89,12 +87,12 @@ export function isMounted(component: React$Component<any, any>): boolean {
87 if (!fiber) {
88 return false;
89 }
92 - return isFiberMountedImpl(fiber) === MOUNTED;
90 + return getNearestMountedFiber(fiber) === fiber;
91 }
92
93 function assertIsMounted(fiber) {
94 invariant(
97 - isFiberMountedImpl(fiber) === MOUNTED,
95 + getNearestMountedFiber(fiber) === fiber,
96 'Unable to find node on an unmounted component.',
97 );
98 }
@@ -103,12 +101,12 @@ export function findCurrentFiberUsingSlowPath(fiber: Fiber): Fiber | null {
101 let alternate = fiber.alternate;
102 if (!alternate) {
103 // If there is no alternate, then we only need to check if it is mounted.
106 - const state = isFiberMountedImpl(fiber);
104 + const nearestMounted = getNearestMountedFiber(fiber);
105 invariant(
108 - state !== UNMOUNTED,
106 + nearestMounted !== null,
107 'Unable to find node on an unmounted component.',
108 );
111 - if (state === MOUNTING) {
109 + if (nearestMounted !== fiber) {
110 return null;
111 }
112 return fiber;
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+1
@@ -126,6 +126,7 @@ export const getNextHydratableSibling = $$$hostConfig.getNextHydratableSibling;
126 export const getFirstHydratableChild = $$$hostConfig.getFirstHydratableChild;
127 export const hydrateInstance = $$$hostConfig.hydrateInstance;
128 export const hydrateTextInstance = $$$hostConfig.hydrateTextInstance;
129 +export const hydrateSuspenseInstance = $$$hostConfig.hydrateSuspenseInstance;
130 export const getNextHydratableInstanceAfterSuspenseInstance =
131 $$$hostConfig.getNextHydratableInstanceAfterSuspenseInstance;
132 export const clearSuspenseBoundary = $$$hostConfig.clearSuspenseBoundary;
packages/shared/HostConfigWithNoHydration.js
+1
@@ -34,6 +34,7 @@ export const getNextHydratableSibling = shim;
34 export const getFirstHydratableChild = shim;
35 export const hydrateInstance = shim;
36 export const hydrateTextInstance = shim;
37 +export const hydrateSuspenseInstance = shim;
38 export const getNextHydratableInstanceAfterSuspenseInstance = shim;
39 export const clearSuspenseBoundary = shim;
40 export const clearSuspenseBoundaryFromContainer = shim;
scripts/error-codes/codes.json
+2 -1
@@ -341,5 +341,6 @@
341 "340": "Threw in newly mounted dehydrated component. This is likely a bug in React. Please file an issue.",
342 "341": "We just came from a parent so we must have had a parent. This is a bug in React.",
343 "342": "A React component suspended while rendering, but no fallback UI was specified.\n\nAdd a <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display.",
344 - "343": "ReactDOMServer does not yet support scope components."
344 + "343": "ReactDOMServer does not yet support scope components.",
345 + "344": "Expected prepareToHydrateHostSuspenseInstance() to never be called. This error is likely caused by a bug in React. Please file an issue."
346 }