diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index d31bf6cfc9a..f4e465ea4d3 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -1895,4 +1895,166 @@ describe('ReactDOMServerPartialHydration', () => { document.body.removeChild(container); }); + + it('does not invoke the parent of dehydrated boundary event', async () => { + let suspend = false; + let resolve; + let promise = new Promise(resolvePromise => (resolve = resolvePromise)); + + let clicksOnParent = 0; + let clicksOnChild = 0; + + function Child({text}) { + if (suspend) { + throw promise; + } else { + return ( + { + // The stopPropagation is showing an example why invoking + // the event on only a parent might not be correct. + e.stopPropagation(); + clicksOnChild++; + }}> + Hello + + ); + } + } + + function App() { + return ( +
clicksOnParent++}> + + + +
+ ); + } + + suspend = false; + let finalHTML = ReactDOMServer.renderToString(); + let container = document.createElement('div'); + container.innerHTML = finalHTML; + + // We need this to be in the document since we'll dispatch events on it. + document.body.appendChild(container); + + let span = container.getElementsByTagName('span')[0]; + + // On the client we don't have all data yet but we want to start + // hydrating anyway. + suspend = true; + let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + root.render(); + Scheduler.unstable_flushAll(); + jest.runAllTimers(); + + // We're now partially hydrated. + span.click(); + expect(clicksOnChild).toBe(0); + expect(clicksOnParent).toBe(0); + + // Resolving the promise so that rendering can complete. + suspend = false; + resolve(); + await promise; + + Scheduler.unstable_flushAll(); + jest.runAllTimers(); + + // TODO: With selective hydration the event should've been replayed + // but for now we'll have to issue it again. + act(() => { + span.click(); + }); + + expect(clicksOnChild).toBe(1); + // This will be zero due to the stopPropagation. + expect(clicksOnParent).toBe(0); + + document.body.removeChild(container); + }); + + it('does not invoke an event on a parent tree when a subtree is dehydrated', async () => { + let suspend = false; + let resolve; + let promise = new Promise(resolvePromise => (resolve = resolvePromise)); + + let clicks = 0; + let childSlotRef = React.createRef(); + + function Parent() { + return
clicks++} ref={childSlotRef} />; + } + + function Child({text}) { + if (suspend) { + throw promise; + } else { + return Click me; + } + } + + function App() { + // The root is a Suspense boundary. + return ( + + + + ); + } + + suspend = false; + let finalHTML = ReactDOMServer.renderToString(); + + let parentContainer = document.createElement('div'); + let childContainer = document.createElement('div'); + + // We need this to be in the document since we'll dispatch events on it. + document.body.appendChild(parentContainer); + + // We're going to use a different root as a parent. + // This lets us detect whether an event goes through React's event system. + let parentRoot = ReactDOM.unstable_createRoot(parentContainer); + parentRoot.render(); + Scheduler.unstable_flushAll(); + + childSlotRef.current.appendChild(childContainer); + + childContainer.innerHTML = finalHTML; + + let a = childContainer.getElementsByTagName('a')[0]; + + suspend = true; + + // Hydrate asynchronously. + let root = ReactDOM.unstable_createRoot(childContainer, {hydrate: true}); + root.render(); + jest.runAllTimers(); + Scheduler.unstable_flushAll(); + + // The Suspense boundary is not yet hydrated. + a.click(); + expect(clicks).toBe(0); + + // Resolving the promise so that rendering can complete. + suspend = false; + resolve(); + await promise; + + Scheduler.unstable_flushAll(); + jest.runAllTimers(); + + // We're now full hydrated. + // TODO: With selective hydration the event should've been replayed + // but for now we'll have to issue it again. + act(() => { + a.click(); + }); + + expect(clicks).toBe(1); + + document.body.removeChild(parentContainer); + }); }); diff --git a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js index a533923d5b7..819855bbfce 100644 --- a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js @@ -586,4 +586,62 @@ describe('ReactDOMServerHydration', () => { document.body.removeChild(container); }); + + it('does not invoke an event on a parent tree when a subtree is hydrating', () => { + let clicks = 0; + let childSlotRef = React.createRef(); + + function Parent() { + return
clicks++} ref={childSlotRef} />; + } + + function App() { + return ( +
+ Click me +
+ ); + } + + let finalHTML = ReactDOMServer.renderToString(); + + let parentContainer = document.createElement('div'); + let childContainer = document.createElement('div'); + + // We need this to be in the document since we'll dispatch events on it. + document.body.appendChild(parentContainer); + + // We're going to use a different root as a parent. + // This lets us detect whether an event goes through React's event system. + let parentRoot = ReactDOM.unstable_createRoot(parentContainer); + parentRoot.render(); + Scheduler.unstable_flushAll(); + + childSlotRef.current.appendChild(childContainer); + + childContainer.innerHTML = finalHTML; + + let a = childContainer.getElementsByTagName('a')[0]; + + // Hydrate asynchronously. + let root = ReactDOM.unstable_createRoot(childContainer, {hydrate: true}); + root.render(); + // Nothing has rendered so far. + + a.click(); + expect(clicks).toBe(0); + + Scheduler.unstable_flushAll(); + + // We're now full hydrated. + // TODO: With selective hydration the event should've been replayed + // but for now we'll have to issue it again. + act(() => { + a.click(); + }); + + expect(clicks).toBe(1); + + document.body.removeChild(parentContainer); + }); }); diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index e11756cb6bf..80ee16b7bf0 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -70,6 +70,7 @@ import { getNodeFromInstance, getFiberCurrentPropsFromNode, getClosestInstanceFromNode, + markContainerAsRoot, } from './ReactDOMComponentTree'; import {restoreControlledState} from './ReactDOMComponent'; import {dispatchEvent} from '../events/ReactDOMEventListener'; @@ -375,6 +376,7 @@ function ReactSyncRoot( (options != null && options.hydrationOptions) || null; const root = createContainer(container, tag, hydrate, hydrationCallbacks); this._internalRoot = root; + markContainerAsRoot(root.current, container); } function ReactRoot(container: DOMContainer, options: void | RootOptions) { @@ -388,6 +390,7 @@ function ReactRoot(container: DOMContainer, options: void | RootOptions) { hydrationCallbacks, ); this._internalRoot = root; + markContainerAsRoot(root.current, container); } ReactRoot.prototype.render = ReactSyncRoot.prototype.render = function( diff --git a/packages/react-dom/src/client/ReactDOMComponentTree.js b/packages/react-dom/src/client/ReactDOMComponentTree.js index 8d3105ae443..2fe287ec8b5 100644 --- a/packages/react-dom/src/client/ReactDOMComponentTree.js +++ b/packages/react-dom/src/client/ReactDOMComponentTree.js @@ -8,43 +8,93 @@ import {HostComponent, HostText} from 'shared/ReactWorkTags'; import invariant from 'shared/invariant'; +import {getParentSuspenseInstance} from './ReactDOMHostConfig'; + const randomKey = Math.random() .toString(36) .slice(2); const internalInstanceKey = '__reactInternalInstance$' + randomKey; const internalEventHandlersKey = '__reactEventHandlers$' + randomKey; +const internalContainerInstanceKey = '__reactContainere$' + randomKey; export function precacheFiberNode(hostInst, node) { node[internalInstanceKey] = hostInst; } -/** - * Given a DOM node, return the closest ReactDOMComponent or - * ReactDOMTextComponent instance ancestor. - */ -export function getClosestInstanceFromNode(node) { - let inst = node[internalInstanceKey]; - if (inst) { - return inst; - } +export function markContainerAsRoot(hostRoot, node) { + node[internalContainerInstanceKey] = hostRoot; +} - do { - node = node.parentNode; - if (node) { - inst = node[internalInstanceKey]; - } else { - // Top of the tree. This node must not be part of a React tree (or is - // unmounted, potentially). - return null; +// Given a DOM node, return the closest HostComponent or HostText fiber ancestor. +// If the target node is part of a hydrated or not yet rendered subtree, then +// this may also return a SuspenseComponent or HostRoot to indicate that. +// Conceptually the HostRoot fiber is a child of the Container node. So if you +// pass the Container node as the targetNode, you wiill not actually get the +// HostRoot back. To get to the HostRoot, you need to pass a child of it. +// The same thing applies to Suspense boundaries. +export function getClosestInstanceFromNode(targetNode) { + let targetInst = targetNode[internalInstanceKey]; + if (targetInst) { + // Don't return HostRoot or SuspenseComponent here. + return targetInst; + } + // If the direct event target isn't a React owned DOM node, we need to look + // to see if one of its parents is a React owned DOM node. + let parentNode = targetNode.parentNode; + while (parentNode) { + // We'll check if this is a container root that could include + // React nodes in the future. We need to check this first because + // if we're a child of a dehydrated container, we need to first + // find that inner container before moving on to finding the parent + // instance. Note that we don't check this field on the targetNode + // itself because the fibers are conceptually between the container + // node and the first child. It isn't surrounding the container node. + targetInst = parentNode[internalContainerInstanceKey]; + if (targetInst) { + // If so, we return the HostRoot Fiber. + return targetInst; } - } while (!inst); + targetInst = parentNode[internalInstanceKey]; + if (targetInst) { + // Since this wasn't the direct target of the event, we might have + // stepped past dehydrated DOM nodes to get here. However they could + // also have been non-React nodes. We need to answer which one. - let tag = inst.tag; - switch (tag) { - case HostComponent: - case HostText: - // In Fiber, this will always be the deepest root. - return inst; + // If we the instance doesn't have any children, then there can't be + // a nested suspense boundary within it. So we can use this as a fast + // bailout. Most of the time, when people add non-React children to + // the tree, it is using a ref to a child-less DOM node. + // We only need to check one of the fibers because if it has ever + // gone from having children to deleting them or vice versa it would + // have deleted the dehydrated boundary nested inside already. + if (targetInst.child !== null) { + // Next we need to figure out if the node that skipped past is + // nested within a dehydrated boundary and if so, which one. + let suspenseInstance = getParentSuspenseInstance(targetNode); + if (suspenseInstance !== null) { + // We found a suspense instance. That means that we haven't + // hydrated it yet. Even though we leave the comments in the + // DOM after hydrating, and there are boundaries in the DOM + // that could already be hydrated, we wouldn't have found them + // through this pass since if the target is hydrated it would + // have had an internalInstanceKey on it. + // Let's get the fiber associated with the SuspenseComponent + // as the deepest instance. + let targetSuspenseInst = suspenseInstance[internalInstanceKey]; + if (targetSuspenseInst) { + return targetSuspenseInst; + } + // If we don't find a Fiber on the comment, it might be because + // we haven't gotten to hydrate it yet. That should mean that + // the parent component also hasn't hydrated yet but we can + // just return that since it will bail out on the isMounted + // check. + } + } + return targetInst; + } + targetNode = parentNode; + parentNode = targetNode.parentNode; } return null; } diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 6b95e63ac9d..1f7604eb605 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -673,6 +673,13 @@ export function hydrateTextInstance( return diffHydratedText(textInstance, text); } +export function hydrateSuspenseInstance( + suspenseInstance: SuspenseInstance, + internalInstanceHandle: Object, +) { + precacheFiberNode(internalInstanceHandle, suspenseInstance); +} + export function getNextHydratableInstanceAfterSuspenseInstance( suspenseInstance: SuspenseInstance, ): null | HydratableInstance { @@ -704,6 +711,39 @@ export function getNextHydratableInstanceAfterSuspenseInstance( return null; } +// Returns the SuspenseInstance if this node is a direct child of a +// SuspenseInstance. I.e. if its previous sibling is a Comment with +// SUSPENSE_x_START_DATA. Otherwise, null. +export function getParentSuspenseInstance( + targetInstance: Instance, +): null | SuspenseInstance { + let node = targetInstance.previousSibling; + // Skip past all nodes within this suspense boundary. + // There might be nested nodes so we need to keep track of how + // deep we are and only break out when we're back on top. + let depth = 0; + while (node) { + if (node.nodeType === COMMENT_NODE) { + let data = ((node: any).data: string); + if ( + data === SUSPENSE_START_DATA || + data === SUSPENSE_FALLBACK_START_DATA || + data === SUSPENSE_PENDING_START_DATA + ) { + if (depth === 0) { + return ((node: any): SuspenseInstance); + } else { + depth--; + } + } else if (data === SUSPENSE_END_DATA) { + depth++; + } + } + node = node.previousSibling; + } + return null; +} + export function didNotMatchHydratedContainerTextInstance( parentContainer: Container, textInstance: TextInstance, diff --git a/packages/react-dom/src/events/EnterLeaveEventPlugin.js b/packages/react-dom/src/events/EnterLeaveEventPlugin.js index c16db1c1111..41330064881 100644 --- a/packages/react-dom/src/events/EnterLeaveEventPlugin.js +++ b/packages/react-dom/src/events/EnterLeaveEventPlugin.js @@ -19,6 +19,7 @@ import { getClosestInstanceFromNode, getNodeFromInstance, } from '../client/ReactDOMComponentTree'; +import {HostComponent, HostText} from 'shared/ReactWorkTags'; const eventTypes = { mouseEnter: { @@ -89,6 +90,9 @@ const EnterLeaveEventPlugin = { from = targetInst; const related = nativeEvent.relatedTarget || nativeEvent.toElement; to = related ? getClosestInstanceFromNode(related) : null; + if (to !== null && to.tag !== HostComponent && to.tag !== HostText) { + to = null; + } } else { // Moving to a node from outside the window. from = null; diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index 0c8a3f99490..b2d7e33b5d3 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -22,8 +22,13 @@ import { } from 'legacy-events/ReactGenericBatching'; import {runExtractedPluginEventsInBatch} from 'legacy-events/EventPluginHub'; import {dispatchEventForResponderEventSystem} from '../events/DOMEventResponderSystem'; -import {isFiberMounted} from 'react-reconciler/reflection'; -import {HostRoot} from 'shared/ReactWorkTags'; +import {getNearestMountedFiber} from 'react-reconciler/reflection'; +import { + HostRoot, + SuspenseComponent, + HostComponent, + HostText, +} from 'shared/ReactWorkTags'; import { type EventSystemFlags, PLUGIN_EVENT_SYSTEM, @@ -77,6 +82,9 @@ type BookKeepingInstance = { * other). If React trees are not nested, returns null. */ function findRootContainerNode(inst) { + if (inst.tag === HostRoot) { + return inst.stateNode.containerInfo; + } // TODO: It may be a good idea to cache this to prevent unnecessary DOM // traversal, but caching is difficult to do correctly without using a // mutation observer to listen for all DOM changes. @@ -141,7 +149,10 @@ function handleTopLevel(bookKeeping: BookKeepingInstance) { if (!root) { break; } - bookKeeping.ancestors.push(ancestor); + const tag = ancestor.tag; + if (tag === HostComponent || tag === HostText) { + bookKeeping.ancestors.push(ancestor); + } ancestor = getClosestInstanceFromNode(root); } while (ancestor); @@ -311,16 +322,34 @@ export function dispatchEvent( const nativeEventTarget = getEventTarget(nativeEvent); let targetInst = getClosestInstanceFromNode(nativeEventTarget); - if ( - targetInst !== null && - typeof targetInst.tag === 'number' && - !isFiberMounted(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; + if (targetInst !== null) { + let nearestMounted = getNearestMountedFiber(targetInst); + if (nearestMounted === null) { + // This tree has been unmounted already. + targetInst = null; + } else { + const tag = nearestMounted.tag; + if (tag === SuspenseComponent) { + // 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 (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; + } + } } if (enableFlareAPI) { diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index c75a4cebec3..945191c4454 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -938,6 +938,9 @@ function updateHostRoot(current, workInProgress, renderExpirationTime) { } const root: FiberRoot = workInProgress.stateNode; if ( + // TODO: This is a bug because if we render null after having hydrating, + // we'll reenter hydration state at the next update which will then + // trigger hydration warnings. (current === null || current.child === null) && root.hydrate && enterHydrationState(workInProgress) @@ -947,20 +950,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. diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 8f9addc35a1..7c6e0f90fcf 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -107,6 +107,7 @@ import {popProvider} from './ReactFiberNewContext'; import { prepareToHydrateHostInstance, prepareToHydrateHostTextInstance, + prepareToHydrateHostSuspenseInstance, popHydrationState, resetHydrationState, } from './ReactFiberHydrationContext'; @@ -819,6 +820,7 @@ function completeWork( 'A dehydrated suspense component was completed without a hydrated node. ' + 'This is probably a bug in React.', ); + prepareToHydrateHostSuspenseInstance(workInProgress); if (enableSchedulerTracing) { markSpawnedWork(Never); } diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.js b/packages/react-reconciler/src/ReactFiberHydrationContext.js index 30a3c20dd6e..8ae0f7b8516 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.js @@ -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 { @@ -41,6 +41,7 @@ import { getFirstHydratableChild, hydrateInstance, hydrateTextInstance, + hydrateSuspenseInstance, getNextHydratableInstanceAfterSuspenseInstance, didNotMatchHydratedContainerTextInstance, didNotMatchHydratedTextInstance, @@ -139,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: { @@ -370,6 +371,26 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): boolean { return shouldUpdate; } +function prepareToHydrateHostSuspenseInstance(fiber: Fiber): void { + if (!supportsHydration) { + invariant( + false, + 'Expected prepareToHydrateHostSuspenseInstance() to never be called. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + } + + let suspenseState: null | SuspenseState = fiber.memoizedState; + let suspenseInstance: null | SuspenseInstance = + suspenseState !== null ? suspenseState.dehydrated : null; + invariant( + suspenseInstance, + 'Expected to have a hydrated suspense instance. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + hydrateSuspenseInstance(suspenseInstance, fiber); +} + function skipPastDehydratedSuspenseInstance( fiber: Fiber, ): null | HydratableInstance { @@ -471,5 +492,6 @@ export { tryToClaimNextHydratableInstance, prepareToHydrateHostInstance, prepareToHydrateHostTextInstance, + prepareToHydrateHostSuspenseInstance, popHydrationState, }; diff --git a/packages/react-reconciler/src/ReactFiberTreeReflection.js b/packages/react-reconciler/src/ReactFiberTreeReflection.js index 7b9e687de69..8d5c360a23c 100644 --- a/packages/react-reconciler/src/ReactFiberTreeReflection.js +++ b/packages/react-reconciler/src/ReactFiberTreeReflection.js @@ -28,14 +28,9 @@ 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. @@ -43,7 +38,10 @@ function isFiberMountedImpl(fiber: Fiber): MountState { 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); @@ -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): boolean { @@ -89,12 +87,12 @@ export function isMounted(component: React$Component): 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.', ); } @@ -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; diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js index 2f234a1cfe9..61c71588a42 100644 --- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js @@ -126,6 +126,7 @@ export const getNextHydratableSibling = $$$hostConfig.getNextHydratableSibling; export const getFirstHydratableChild = $$$hostConfig.getFirstHydratableChild; export const hydrateInstance = $$$hostConfig.hydrateInstance; export const hydrateTextInstance = $$$hostConfig.hydrateTextInstance; +export const hydrateSuspenseInstance = $$$hostConfig.hydrateSuspenseInstance; export const getNextHydratableInstanceAfterSuspenseInstance = $$$hostConfig.getNextHydratableInstanceAfterSuspenseInstance; export const clearSuspenseBoundary = $$$hostConfig.clearSuspenseBoundary; diff --git a/packages/shared/HostConfigWithNoHydration.js b/packages/shared/HostConfigWithNoHydration.js index 1be5f0b8a98..7fa5a025b6a 100644 --- a/packages/shared/HostConfigWithNoHydration.js +++ b/packages/shared/HostConfigWithNoHydration.js @@ -34,6 +34,7 @@ export const getNextHydratableSibling = shim; export const getFirstHydratableChild = shim; export const hydrateInstance = shim; export const hydrateTextInstance = shim; +export const hydrateSuspenseInstance = shim; export const getNextHydratableInstanceAfterSuspenseInstance = shim; export const clearSuspenseBoundary = shim; export const clearSuspenseBoundaryFromContainer = shim; diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 5946ec50069..5cac8a18d89 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -341,5 +341,6 @@ "340": "Threw in newly mounted dehydrated component. This is likely a bug in React. Please file an issue.", "341": "We just came from a parent so we must have had a parent. This is a bug in React.", "342": "A React component suspended while rendering, but no fallback UI was specified.\n\nAdd a component higher in the tree to provide a loading indicator or placeholder to display.", - "343": "ReactDOMServer does not yet support scope components." + "343": "ReactDOMServer does not yet support scope components.", + "344": "Expected prepareToHydrateHostSuspenseInstance() to never be called. This error is likely caused by a bug in React. Please file an issue." }