/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import type {ReactScopeInstance} from 'shared/ReactTypes'; import type { ReactDOMEventHandle, ReactDOMEventHandleListener, } from '../shared/ReactDOMTypes'; import type { Container, TextInstance, Instance, SuspenseInstance, Props, } from './ReactDOMHostConfig'; import { HostComponent, HostText, HostRoot, SuspenseComponent, } from 'react-reconciler/src/ReactWorkTags'; import {getParentSuspenseInstance} from './ReactDOMHostConfig'; import invariant from 'shared/invariant'; import {enableScopeAPI} from 'shared/ReactFeatureFlags'; const randomKey = Math.random() .toString(36) .slice(2); const internalInstanceKey = '__reactFiber$' + randomKey; const internalPropsKey = '__reactProps$' + randomKey; const internalContainerInstanceKey = '__reactContainer$' + randomKey; const internalEventHandlersKey = '__reactEvents$' + randomKey; const internalEventHandlerListenersKey = '__reactListeners$' + randomKey; const internalEventHandlesSetKey = '__reactHandles$' + randomKey; export function detachDeletedInstance(node: Instance): void { // TODO: This function is only called on host components. I don't think all of // these fields are relevant. delete (node: any)[internalInstanceKey]; delete (node: any)[internalPropsKey]; delete (node: any)[internalEventHandlersKey]; delete (node: any)[internalEventHandlerListenersKey]; delete (node: any)[internalEventHandlesSetKey]; } export function precacheFiberNode( hostInst: Fiber, node: Instance | TextInstance | SuspenseInstance | ReactScopeInstance, ): void { (node: any)[internalInstanceKey] = hostInst; } export function markContainerAsRoot(hostRoot: Fiber, node: Container): void { node[internalContainerInstanceKey] = hostRoot; } export function unmarkContainerAsRoot(node: Container): void { node[internalContainerInstanceKey] = null; } export function isContainerMarkedAsRoot(node: Container): boolean { return !!node[internalContainerInstanceKey]; } // 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 will 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: Node): null | Fiber { let targetInst = (targetNode: any)[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. // If it's not a container, we check if it's an instance. targetInst = (parentNode: any)[internalContainerInstanceKey] || (parentNode: any)[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. // 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. // Normally we'd 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. // However, since the HostRoot starts out with an alternate it might // have one on the alternate so we need to check in case this was a // root. const alternate = targetInst.alternate; if ( targetInst.child !== null || (alternate !== null && alternate.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); while (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. const 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. There might still be a // parent boundary that hasn't above this one so we need to find // the outer most that is known. suspenseInstance = getParentSuspenseInstance(suspenseInstance); // If we don't find one, then that should mean that the parent // host component also hasn't hydrated yet. We can return it // below since it will bail out on the isMounted check later. } } return targetInst; } targetNode = parentNode; parentNode = targetNode.parentNode; } return null; } /** * Given a DOM node, return the ReactDOMComponent or ReactDOMTextComponent * instance, or null if the node was not rendered by this React. */ export function getInstanceFromNode(node: Node): Fiber | null { const inst = (node: any)[internalInstanceKey] || (node: any)[internalContainerInstanceKey]; if (inst) { if ( inst.tag === HostComponent || inst.tag === HostText || inst.tag === SuspenseComponent || inst.tag === HostRoot ) { return inst; } else { return null; } } return null; } /** * Given a ReactDOMComponent or ReactDOMTextComponent, return the corresponding * DOM node. */ export function getNodeFromInstance(inst: Fiber): Instance | TextInstance { if (inst.tag === HostComponent || inst.tag === HostText) { // In Fiber this, is just the state node right now. We assume it will be // a host component or host text. return inst.stateNode; } // Without this first invariant, passing a non-DOM-component triggers the next // invariant for a missing parent, which is super confusing. invariant(false, 'getNodeFromInstance: Invalid argument.'); } export function getFiberCurrentPropsFromNode( node: Instance | TextInstance | SuspenseInstance, ): Props { return (node: any)[internalPropsKey] || null; } export function updateFiberProps( node: Instance | TextInstance | SuspenseInstance, props: Props, ): void { (node: any)[internalPropsKey] = props; } export function getEventListenerSet(node: EventTarget): Set { let elementListenerSet = (node: any)[internalEventHandlersKey]; if (elementListenerSet === undefined) { elementListenerSet = (node: any)[internalEventHandlersKey] = new Set(); } return elementListenerSet; } export function getFiberFromScopeInstance( scope: ReactScopeInstance, ): null | Fiber { if (enableScopeAPI) { return (scope: any)[internalInstanceKey] || null; } return null; } export function setEventHandlerListeners( scope: EventTarget | ReactScopeInstance, listeners: Set, ): void { (scope: any)[internalEventHandlerListenersKey] = listeners; } export function getEventHandlerListeners( scope: EventTarget | ReactScopeInstance, ): null | Set { return (scope: any)[internalEventHandlerListenersKey] || null; } export function addEventHandleToTarget( target: EventTarget | ReactScopeInstance, eventHandle: ReactDOMEventHandle, ): void { let eventHandles = (target: any)[internalEventHandlesSetKey]; if (eventHandles === undefined) { eventHandles = (target: any)[internalEventHandlesSetKey] = new Set(); } eventHandles.add(eventHandle); } export function doesTargetHaveEventHandle( target: EventTarget | ReactScopeInstance, eventHandle: ReactDOMEventHandle, ): boolean { const eventHandles = (target: any)[internalEventHandlesSetKey]; if (eventHandles === undefined) { return false; } return eventHandles.has(eventHandle); }