| 1 | /** |
| 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | * |
| 4 | * This source code is licensed under the MIT license found in the |
| 5 | * LICENSE file in the root directory of this source tree. |
| 6 | * |
| 7 | * @flow |
| 8 | */ |
| 9 | |
| 10 | import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; |
| 11 | import type {ReactScopeInstance} from 'shared/ReactTypes'; |
| 12 | import type { |
| 13 | ReactDOMEventHandle, |
| 14 | ReactDOMEventHandleListener, |
| 15 | } from './ReactDOMEventHandleTypes'; |
| 16 | import type { |
| 17 | Container, |
| 18 | TextInstance, |
| 19 | Instance, |
| 20 | ActivityInstance, |
| 21 | SuspenseInstance, |
| 22 | Props, |
| 23 | HoistableRoot, |
| 24 | RootResources, |
| 25 | } from './ReactFiberConfigDOM'; |
| 26 | |
| 27 | import { |
| 28 | HostComponent, |
| 29 | HostHoistable, |
| 30 | HostSingleton, |
| 31 | HostText, |
| 32 | HostRoot, |
| 33 | SuspenseComponent, |
| 34 | ActivityComponent, |
| 35 | } from 'react-reconciler/src/ReactWorkTags'; |
| 36 | |
| 37 | import {getParentHydrationBoundary} from './ReactFiberConfigDOM'; |
| 38 | |
| 39 | import {enableScopeAPI} from 'shared/ReactFeatureFlags'; |
| 40 | |
| 41 | import {enableInternalInstanceMap} from 'shared/ReactFeatureFlags'; |
| 42 | |
| 43 | const randomKey = Math.random().toString(36).slice(2); |
| 44 | const internalInstanceKey = '__reactFiber$' + randomKey; |
| 45 | const internalPropsKey = '__reactProps$' + randomKey; |
| 46 | const internalContainerInstanceKey = '__reactContainer$' + randomKey; |
| 47 | const internalEventHandlersKey = '__reactEvents$' + randomKey; |
| 48 | const internalEventHandlerListenersKey = '__reactListeners$' + randomKey; |
| 49 | const internalEventHandlesSetKey = '__reactHandles$' + randomKey; |
| 50 | const internalRootNodeResourcesKey = '__reactResources$' + randomKey; |
| 51 | const internalHoistableMarker = '__reactMarker$' + randomKey; |
| 52 | const internalScrollTimer = '__reactScroll$' + randomKey; |
| 53 | const internalLoadPendingKey = '__reactLoad$' + randomKey; |
| 54 | |
| 55 | type InstanceUnion = |
| 56 | | Instance |
| 57 | | TextInstance |
| 58 | | SuspenseInstance |
| 59 | | ActivityInstance |
| 60 | | ReactScopeInstance |
| 61 | | Container; |
| 62 | |
| 63 | const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; |
| 64 | const internalInstanceMap: |
| 65 | | WeakMap<InstanceUnion, Fiber> |
| 66 | | Map<InstanceUnion, Fiber> = new PossiblyWeakMap(); |
| 67 | const internalPropsMap: |
| 68 | | WeakMap<InstanceUnion, Props> |
| 69 | | Map<InstanceUnion, Props> = new PossiblyWeakMap(); |
| 70 | |
| 71 | export function detachDeletedInstance(node: Instance): void { |
| 72 | // Don't delete the event listener set. The native event listeners it tracks |
| 73 | // remain attached to the node, so this bookkeeping needs to last for the |
| 74 | // lifetime of the node to prevent duplicate listeners if it is reused. |
| 75 | if (enableInternalInstanceMap) { |
| 76 | internalInstanceMap.delete(node); |
| 77 | internalPropsMap.delete(node); |
| 78 | delete (node as any)[internalEventHandlerListenersKey]; |
| 79 | delete (node as any)[internalEventHandlesSetKey]; |
| 80 | delete (node as any)[internalRootNodeResourcesKey]; |
| 81 | if (__DEV__) { |
| 82 | delete (node as any)[internalInstanceKey]; |
| 83 | } |
| 84 | return; |
| 85 | } |
| 86 | // TODO: This function is only called on host components. I don't think all of |
| 87 | // these fields are relevant. |
| 88 | delete (node as any)[internalInstanceKey]; |
| 89 | delete (node as any)[internalPropsKey]; |
| 90 | delete (node as any)[internalEventHandlerListenersKey]; |
| 91 | delete (node as any)[internalEventHandlesSetKey]; |
| 92 | } |
| 93 | |
| 94 | export function precacheFiberNode( |
| 95 | hostInst: Fiber, |
| 96 | node: |
| 97 | | Instance |
| 98 | | TextInstance |
| 99 | | SuspenseInstance |
| 100 | | ActivityInstance |
| 101 | | ReactScopeInstance, |
| 102 | ): void { |
| 103 | if (enableInternalInstanceMap) { |
| 104 | internalInstanceMap.set(node, hostInst); |
| 105 | if (__DEV__) { |
| 106 | (node as any)[internalInstanceKey] = hostInst; |
| 107 | } |
| 108 | return; |
| 109 | } |
| 110 | (node as any)[internalInstanceKey] = hostInst; |
| 111 | } |
| 112 | |
| 113 | export function markContainerAsRoot(hostRoot: Fiber, node: Container): void { |
| 114 | // $FlowFixMe[prop-missing] |
| 115 | node[internalContainerInstanceKey] = hostRoot; |
| 116 | } |
| 117 | |
| 118 | export function unmarkContainerAsRoot(node: Container): void { |
| 119 | // $FlowFixMe[prop-missing] |
| 120 | node[internalContainerInstanceKey] = null; |
| 121 | } |
| 122 | |
| 123 | export function isContainerMarkedAsRoot(node: Container): boolean { |
| 124 | // $FlowFixMe[prop-missing] |
| 125 | return !!node[internalContainerInstanceKey]; |
| 126 | } |
| 127 | |
| 128 | // Given a DOM node, return the closest HostComponent or HostText fiber ancestor. |
| 129 | // If the target node is part of a hydrated or not yet rendered subtree, then |
| 130 | // this may also return a SuspenseComponent, ActivityComponent or HostRoot to |
| 131 | // indicate that. |
| 132 | // Conceptually the HostRoot fiber is a child of the Container node. So if you |
| 133 | // pass the Container node as the targetNode, you will not actually get the |
| 134 | // HostRoot back. To get to the HostRoot, you need to pass a child of it. |
| 135 | // The same thing applies to Suspense and Activity boundaries. |
| 136 | export function getClosestInstanceFromNode(targetNode: Node): null | Fiber { |
| 137 | let targetInst: void | Fiber; |
| 138 | if (enableInternalInstanceMap) { |
| 139 | targetInst = internalInstanceMap.get(targetNode as any as InstanceUnion); |
| 140 | } else { |
| 141 | targetInst = (targetNode as any)[internalInstanceKey]; |
| 142 | } |
| 143 | if (targetInst) { |
| 144 | // Don't return HostRoot, SuspenseComponent or ActivityComponent here. |
| 145 | return targetInst; |
| 146 | } |
| 147 | // If the direct event target isn't a React owned DOM node, we need to look |
| 148 | // to see if one of its parents is a React owned DOM node. |
| 149 | let parentNode = targetNode.parentNode; |
| 150 | while (parentNode) { |
| 151 | // We'll check if this is a container root that could include |
| 152 | // React nodes in the future. We need to check this first because |
| 153 | // if we're a child of a dehydrated container, we need to first |
| 154 | // find that inner container before moving on to finding the parent |
| 155 | // instance. Note that we don't check this field on the targetNode |
| 156 | // itself because the fibers are conceptually between the container |
| 157 | // node and the first child. It isn't surrounding the container node. |
| 158 | // If it's not a container, we check if it's an instance. |
| 159 | if (enableInternalInstanceMap) { |
| 160 | targetInst = |
| 161 | (parentNode as any)[internalContainerInstanceKey] || |
| 162 | internalInstanceMap.get(parentNode as any as InstanceUnion); |
| 163 | } else { |
| 164 | targetInst = |
| 165 | (parentNode as any)[internalContainerInstanceKey] || |
| 166 | (parentNode as any)[internalInstanceKey]; |
| 167 | } |
| 168 | if (targetInst) { |
| 169 | // Since this wasn't the direct target of the event, we might have |
| 170 | // stepped past dehydrated DOM nodes to get here. However they could |
| 171 | // also have been non-React nodes. We need to answer which one. |
| 172 | |
| 173 | // If we the instance doesn't have any children, then there can't be |
| 174 | // a nested suspense boundary within it. So we can use this as a fast |
| 175 | // bailout. Most of the time, when people add non-React children to |
| 176 | // the tree, it is using a ref to a child-less DOM node. |
| 177 | // Normally we'd only need to check one of the fibers because if it |
| 178 | // has ever gone from having children to deleting them or vice versa |
| 179 | // it would have deleted the dehydrated boundary nested inside already. |
| 180 | // However, since the HostRoot starts out with an alternate it might |
| 181 | // have one on the alternate so we need to check in case this was a |
| 182 | // root. |
| 183 | const alternate = targetInst.alternate; |
| 184 | if ( |
| 185 | targetInst.child !== null || |
| 186 | (alternate !== null && alternate.child !== null) |
| 187 | ) { |
| 188 | // Next we need to figure out if the node that skipped past is |
| 189 | // nested within a dehydrated boundary and if so, which one. |
| 190 | let hydrationInstance = getParentHydrationBoundary(targetNode); |
| 191 | while (hydrationInstance !== null) { |
| 192 | // We found a suspense instance. That means that we haven't |
| 193 | // hydrated it yet. Even though we leave the comments in the |
| 194 | // DOM after hydrating, and there are boundaries in the DOM |
| 195 | // that could already be hydrated, we wouldn't have found them |
| 196 | // through this pass since if the target is hydrated it would |
| 197 | // have had an internalInstanceKey on it. |
| 198 | // Let's get the fiber associated with the SuspenseComponent |
| 199 | // as the deepest instance. |
| 200 | const targetFiber = enableInternalInstanceMap |
| 201 | ? internalInstanceMap.get(hydrationInstance) |
| 202 | : // $FlowFixMe[prop-missing] |
| 203 | hydrationInstance[internalInstanceKey]; |
| 204 | if (targetFiber) { |
| 205 | return targetFiber; |
| 206 | } |
| 207 | // If we don't find a Fiber on the comment, it might be because |
| 208 | // we haven't gotten to hydrate it yet. There might still be a |
| 209 | // parent boundary that hasn't above this one so we need to find |
| 210 | // the outer most that is known. |
| 211 | hydrationInstance = getParentHydrationBoundary(hydrationInstance); |
| 212 | // If we don't find one, then that should mean that the parent |
| 213 | // host component also hasn't hydrated yet. We can return it |
| 214 | // below since it will bail out on the isMounted check later. |
| 215 | } |
| 216 | } |
| 217 | return targetInst; |
| 218 | } |
| 219 | targetNode = parentNode; |
| 220 | parentNode = targetNode.parentNode; |
| 221 | } |
| 222 | return null; |
| 223 | } |
| 224 | |
| 225 | /** |
| 226 | * Given a DOM node, return the ReactDOMComponent or ReactDOMTextComponent |
| 227 | * instance, or null if the node was not rendered by this React. |
| 228 | */ |
| 229 | export function getInstanceFromNode(node: Node): Fiber | null { |
| 230 | let inst: void | null | Fiber; |
| 231 | if (enableInternalInstanceMap) { |
| 232 | inst = |
| 233 | internalInstanceMap.get(node as any as InstanceUnion) || |
| 234 | (node as any)[internalContainerInstanceKey]; |
| 235 | } else { |
| 236 | inst = |
| 237 | (node as any)[internalInstanceKey] || |
| 238 | (node as any)[internalContainerInstanceKey]; |
| 239 | } |
| 240 | if (inst) { |
| 241 | const tag = inst.tag; |
| 242 | if ( |
| 243 | tag === HostComponent || |
| 244 | tag === HostText || |
| 245 | tag === SuspenseComponent || |
| 246 | tag === ActivityComponent || |
| 247 | tag === HostHoistable || |
| 248 | tag === HostSingleton || |
| 249 | tag === HostRoot |
| 250 | ) { |
| 251 | return inst; |
| 252 | } else { |
| 253 | return null; |
| 254 | } |
| 255 | } |
| 256 | return null; |
| 257 | } |
| 258 | |
| 259 | /** |
| 260 | * Given a ReactDOMComponent or ReactDOMTextComponent, return the corresponding |
| 261 | * DOM node. |
| 262 | */ |
| 263 | export function getNodeFromInstance(inst: Fiber): Instance | TextInstance { |
| 264 | const tag = inst.tag; |
| 265 | if ( |
| 266 | tag === HostComponent || |
| 267 | tag === HostHoistable || |
| 268 | tag === HostSingleton || |
| 269 | tag === HostText |
| 270 | ) { |
| 271 | // In Fiber this, is just the state node right now. We assume it will be |
| 272 | // a host component or host text. |
| 273 | return inst.stateNode; |
| 274 | } |
| 275 | |
| 276 | // Without this first invariant, passing a non-DOM-component triggers the next |
| 277 | // invariant for a missing parent, which is super confusing. |
| 278 | throw new Error('getNodeFromInstance: Invalid argument.'); |
| 279 | } |
| 280 | |
| 281 | export function getFiberCurrentPropsFromNode( |
| 282 | node: |
| 283 | | Container |
| 284 | | Instance |
| 285 | | TextInstance |
| 286 | | SuspenseInstance |
| 287 | | ActivityInstance, |
| 288 | ): Props | null { |
| 289 | if (enableInternalInstanceMap) { |
| 290 | return internalPropsMap.get(node) || null; |
| 291 | } |
| 292 | return (node as any)[internalPropsKey] || null; |
| 293 | } |
| 294 | |
| 295 | export function updateFiberProps(node: Instance, props: Props): void { |
| 296 | if (enableInternalInstanceMap) { |
| 297 | internalPropsMap.set(node, props); |
| 298 | return; |
| 299 | } |
| 300 | (node as any)[internalPropsKey] = props; |
| 301 | } |
| 302 | |
| 303 | export function getEventListenerSet(node: EventTarget): Set<string> { |
| 304 | let elementListenerSet: Set<string> | void = (node as any)[ |
| 305 | internalEventHandlersKey |
| 306 | ]; |
| 307 | if (elementListenerSet === undefined) { |
| 308 | elementListenerSet = (node as any)[internalEventHandlersKey] = new Set(); |
| 309 | } |
| 310 | return elementListenerSet; |
| 311 | } |
| 312 | |
| 313 | export function getFiberFromScopeInstance( |
| 314 | scope: ReactScopeInstance, |
| 315 | ): null | Fiber { |
| 316 | if (enableScopeAPI) { |
| 317 | if (enableInternalInstanceMap) { |
| 318 | return internalInstanceMap.get(scope as any as InstanceUnion) || null; |
| 319 | } |
| 320 | return (scope as any)[internalInstanceKey] || null; |
| 321 | } |
| 322 | return null; |
| 323 | } |
| 324 | |
| 325 | export function setEventHandlerListeners( |
| 326 | scope: EventTarget | ReactScopeInstance, |
| 327 | listeners: Set<ReactDOMEventHandleListener>, |
| 328 | ): void { |
| 329 | (scope as any)[internalEventHandlerListenersKey] = listeners; |
| 330 | } |
| 331 | |
| 332 | export function getEventHandlerListeners( |
| 333 | scope: EventTarget | ReactScopeInstance, |
| 334 | ): null | Set<ReactDOMEventHandleListener> { |
| 335 | return (scope as any)[internalEventHandlerListenersKey] || null; |
| 336 | } |
| 337 | |
| 338 | export function addEventHandleToTarget( |
| 339 | target: EventTarget | ReactScopeInstance, |
| 340 | eventHandle: ReactDOMEventHandle, |
| 341 | ): void { |
| 342 | let eventHandles = (target as any)[internalEventHandlesSetKey]; |
| 343 | if (eventHandles === undefined) { |
| 344 | eventHandles = (target as any)[internalEventHandlesSetKey] = new Set(); |
| 345 | } |
| 346 | eventHandles.add(eventHandle); |
| 347 | } |
| 348 | |
| 349 | export function doesTargetHaveEventHandle( |
| 350 | target: EventTarget | ReactScopeInstance, |
| 351 | eventHandle: ReactDOMEventHandle, |
| 352 | ): boolean { |
| 353 | const eventHandles = (target as any)[internalEventHandlesSetKey]; |
| 354 | if (eventHandles === undefined) { |
| 355 | return false; |
| 356 | } |
| 357 | return eventHandles.has(eventHandle); |
| 358 | } |
| 359 | |
| 360 | export function getResourcesFromRoot(root: HoistableRoot): RootResources { |
| 361 | let resources = (root as any)[internalRootNodeResourcesKey]; |
| 362 | if (!resources) { |
| 363 | resources = (root as any)[internalRootNodeResourcesKey] = { |
| 364 | hoistableStyles: new Map(), |
| 365 | hoistableScripts: new Map(), |
| 366 | }; |
| 367 | } |
| 368 | return resources; |
| 369 | } |
| 370 | |
| 371 | export function isMarkedHoistable(node: Node): boolean { |
| 372 | return !!(node as any)[internalHoistableMarker]; |
| 373 | } |
| 374 | |
| 375 | export function markNodeAsHoistable(node: Node) { |
| 376 | (node as any)[internalHoistableMarker] = true; |
| 377 | } |
| 378 | |
| 379 | export function getScrollEndTimer(node: EventTarget): ?TimeoutID { |
| 380 | return (node as any)[internalScrollTimer]; |
| 381 | } |
| 382 | |
| 383 | export function setScrollEndTimer(node: EventTarget, timer: TimeoutID): void { |
| 384 | (node as any)[internalScrollTimer] = timer; |
| 385 | } |
| 386 | |
| 387 | export function clearScrollEndTimer(node: EventTarget): void { |
| 388 | (node as any)[internalScrollTimer] = undefined; |
| 389 | } |
| 390 | |
| 391 | export function markNodeAsPendingLoad(node: Node): void { |
| 392 | (node as any)[internalLoadPendingKey] = true; |
| 393 | } |
| 394 | |
| 395 | export function clearPendingLoadOnNode(node: Node): void { |
| 396 | (node as any)[internalLoadPendingKey] = undefined; |
| 397 | } |
| 398 | |
| 399 | export function isNodePendingLoad(node: Node): boolean { |
| 400 | return (node as any)[internalLoadPendingKey] === true; |
| 401 | } |
| 402 | |
| 403 | export function isOwnedInstance(node: Node): boolean { |
| 404 | if (enableInternalInstanceMap) { |
| 405 | return !!( |
| 406 | (node as any)[internalHoistableMarker] || |
| 407 | internalInstanceMap.has(node as any) |
| 408 | ); |
| 409 | } |
| 410 | return !!( |
| 411 | (node as any)[internalHoistableMarker] || (node as any)[internalInstanceKey] |
| 412 | ); |
| 413 | } |