| 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 { |
| 11 | Fiber, |
| 12 | FiberRoot, |
| 13 | SuspenseHydrationCallbacks, |
| 14 | TransitionTracingCallbacks, |
| 15 | } from './ReactInternalTypes'; |
| 16 | import type {RootTag} from './ReactRootTags'; |
| 17 | import type { |
| 18 | Container, |
| 19 | PublicInstance, |
| 20 | RendererInspectionConfig, |
| 21 | } from './ReactFiberConfig'; |
| 22 | import type {ReactNodeList, ReactFormState} from 'shared/ReactTypes'; |
| 23 | import type {Lane} from './ReactFiberLane'; |
| 24 | import type {ActivityState} from './ReactFiberActivityComponent'; |
| 25 | import type {SuspenseState} from './ReactFiberSuspenseComponent'; |
| 26 | |
| 27 | import {LegacyRoot} from './ReactRootTags'; |
| 28 | import { |
| 29 | findCurrentHostFiber, |
| 30 | findCurrentHostFiberWithNoPortals, |
| 31 | } from './ReactFiberTreeReflection'; |
| 32 | import {get as getInstance} from 'shared/ReactInstanceMap'; |
| 33 | import { |
| 34 | HostComponent, |
| 35 | HostSingleton, |
| 36 | ClassComponent, |
| 37 | HostRoot, |
| 38 | SuspenseComponent, |
| 39 | ActivityComponent, |
| 40 | } from './ReactWorkTags'; |
| 41 | import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; |
| 42 | import isArray from 'shared/isArray'; |
| 43 | import { |
| 44 | enableSchedulingProfiler, |
| 45 | disableLegacyMode, |
| 46 | } from 'shared/ReactFeatureFlags'; |
| 47 | import ReactSharedInternals from 'shared/ReactSharedInternals'; |
| 48 | import { |
| 49 | getPublicInstance, |
| 50 | rendererVersion, |
| 51 | rendererPackageName, |
| 52 | extraDevToolsConfig, |
| 53 | } from './ReactFiberConfig'; |
| 54 | import { |
| 55 | findCurrentUnmaskedContext, |
| 56 | processChildContext, |
| 57 | emptyContextObject, |
| 58 | isContextProvider as isLegacyContextProvider, |
| 59 | } from './ReactFiberLegacyContext'; |
| 60 | import {createFiberRoot} from './ReactFiberRoot'; |
| 61 | import {isRootDehydrated} from './ReactFiberShellHydration'; |
| 62 | import { |
| 63 | injectInternals, |
| 64 | markRenderScheduled, |
| 65 | onScheduleRoot, |
| 66 | injectProfilingHooks, |
| 67 | } from './ReactFiberDevToolsHook'; |
| 68 | import {startUpdateTimerByLane} from './ReactProfilerTimer'; |
| 69 | import { |
| 70 | requestUpdateLane, |
| 71 | scheduleUpdateOnFiber, |
| 72 | scheduleInitialHydrationOnRoot, |
| 73 | flushRoot, |
| 74 | batchedUpdates, |
| 75 | flushSyncFromReconciler, |
| 76 | flushSyncWork, |
| 77 | isAlreadyRendering, |
| 78 | deferredUpdates, |
| 79 | discreteUpdates, |
| 80 | flushPendingEffects, |
| 81 | } from './ReactFiberWorkLoop'; |
| 82 | import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates'; |
| 83 | import { |
| 84 | createUpdate, |
| 85 | enqueueUpdate, |
| 86 | entangleTransitions, |
| 87 | } from './ReactFiberClassUpdateQueue'; |
| 88 | import { |
| 89 | isRendering as ReactCurrentFiberIsRendering, |
| 90 | current as ReactCurrentFiberCurrent, |
| 91 | runWithFiberInDEV, |
| 92 | } from './ReactCurrentFiber'; |
| 93 | import {StrictLegacyMode} from './ReactTypeOfMode'; |
| 94 | import { |
| 95 | SyncLane, |
| 96 | SelectiveHydrationLane, |
| 97 | getHighestPriorityPendingLanes, |
| 98 | higherPriorityLane, |
| 99 | getBumpedLaneForHydrationByLane, |
| 100 | claimNextRetryLane, |
| 101 | } from './ReactFiberLane'; |
| 102 | import { |
| 103 | scheduleRefresh, |
| 104 | scheduleRoot, |
| 105 | setRefreshHandler, |
| 106 | } from './ReactFiberHotReloading'; |
| 107 | import ReactVersion from 'shared/ReactVersion'; |
| 108 | export {createPortal} from './ReactPortal'; |
| 109 | export { |
| 110 | createComponentSelector, |
| 111 | createHasPseudoClassSelector, |
| 112 | createRoleSelector, |
| 113 | createTestNameSelector, |
| 114 | createTextSelector, |
| 115 | getFindAllNodesFailureDescription, |
| 116 | findAllNodes, |
| 117 | findBoundingRects, |
| 118 | focusWithin, |
| 119 | observeVisibleRects, |
| 120 | } from './ReactTestSelectors'; |
| 121 | export {startHostTransition} from './ReactFiberHooks'; |
| 122 | export { |
| 123 | defaultOnUncaughtError, |
| 124 | defaultOnCaughtError, |
| 125 | defaultOnRecoverableError, |
| 126 | } from './ReactFiberErrorLogger'; |
| 127 | import {getLabelForLane, TotalLanes} from 'react-reconciler/src/ReactFiberLane'; |
| 128 | import {registerDefaultIndicator} from './ReactFiberAsyncAction'; |
| 129 | |
| 130 | type OpaqueRoot = FiberRoot; |
| 131 | |
| 132 | let didWarnAboutNestedUpdates; |
| 133 | let didWarnAboutFindNodeInStrictMode; |
| 134 | |
| 135 | if (__DEV__) { |
| 136 | didWarnAboutNestedUpdates = false; |
| 137 | didWarnAboutFindNodeInStrictMode = {} as {[string]: boolean}; |
| 138 | } |
| 139 | |
| 140 | function getContextForSubtree( |
| 141 | parentComponent: ?component(...props: any), |
| 142 | ): Object { |
| 143 | if (!parentComponent) { |
| 144 | return emptyContextObject; |
| 145 | } |
| 146 | |
| 147 | const fiber = getInstance(parentComponent); |
| 148 | const parentContext = findCurrentUnmaskedContext(fiber); |
| 149 | |
| 150 | if (fiber.tag === ClassComponent) { |
| 151 | const Component = fiber.type; |
| 152 | if (isLegacyContextProvider(Component)) { |
| 153 | return processChildContext(fiber, Component, parentContext); |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | return parentContext; |
| 158 | } |
| 159 | |
| 160 | function findHostInstance(component: Object): PublicInstance | null { |
| 161 | const fiber = getInstance(component); |
| 162 | if (fiber === undefined) { |
| 163 | if (typeof component.render === 'function') { |
| 164 | throw new Error('Unable to find node on an unmounted component.'); |
| 165 | } else { |
| 166 | const keys = Object.keys(component).join(','); |
| 167 | throw new Error( |
| 168 | `Argument appears to not be a ReactComponent. Keys: ${keys}`, |
| 169 | ); |
| 170 | } |
| 171 | } |
| 172 | const hostFiber = findCurrentHostFiber(fiber); |
| 173 | if (hostFiber === null) { |
| 174 | return null; |
| 175 | } |
| 176 | return getPublicInstance(hostFiber.stateNode); |
| 177 | } |
| 178 | |
| 179 | function findHostInstanceWithWarning( |
| 180 | component: Object, |
| 181 | methodName: string, |
| 182 | ): PublicInstance | null { |
| 183 | if (__DEV__) { |
| 184 | const fiber = getInstance(component); |
| 185 | if (fiber === undefined) { |
| 186 | if (typeof component.render === 'function') { |
| 187 | throw new Error('Unable to find node on an unmounted component.'); |
| 188 | } else { |
| 189 | const keys = Object.keys(component).join(','); |
| 190 | throw new Error( |
| 191 | `Argument appears to not be a ReactComponent. Keys: ${keys}`, |
| 192 | ); |
| 193 | } |
| 194 | } |
| 195 | const hostFiber = findCurrentHostFiber(fiber); |
| 196 | if (hostFiber === null) { |
| 197 | return null; |
| 198 | } |
| 199 | if (hostFiber.mode & StrictLegacyMode) { |
| 200 | const componentName = getComponentNameFromFiber(fiber) || 'Component'; |
| 201 | if (!didWarnAboutFindNodeInStrictMode[componentName]) { |
| 202 | didWarnAboutFindNodeInStrictMode[componentName] = true; |
| 203 | runWithFiberInDEV(hostFiber, () => { |
| 204 | if (fiber.mode & StrictLegacyMode) { |
| 205 | console.error( |
| 206 | '%s is deprecated in StrictMode. ' + |
| 207 | '%s was passed an instance of %s which is inside StrictMode. ' + |
| 208 | 'Instead, add a ref directly to the element you want to reference. ' + |
| 209 | 'Learn more about using refs safely here: ' + |
| 210 | 'https://react.dev/link/strict-mode-find-node', |
| 211 | methodName, |
| 212 | methodName, |
| 213 | componentName, |
| 214 | ); |
| 215 | } else { |
| 216 | console.error( |
| 217 | '%s is deprecated in StrictMode. ' + |
| 218 | '%s was passed an instance of %s which renders StrictMode children. ' + |
| 219 | 'Instead, add a ref directly to the element you want to reference. ' + |
| 220 | 'Learn more about using refs safely here: ' + |
| 221 | 'https://react.dev/link/strict-mode-find-node', |
| 222 | methodName, |
| 223 | methodName, |
| 224 | componentName, |
| 225 | ); |
| 226 | } |
| 227 | }); |
| 228 | } |
| 229 | } |
| 230 | return getPublicInstance(hostFiber.stateNode); |
| 231 | } |
| 232 | return findHostInstance(component); |
| 233 | } |
| 234 | |
| 235 | export function createContainer( |
| 236 | containerInfo: Container, |
| 237 | tag: RootTag, |
| 238 | hydrationCallbacks: null | SuspenseHydrationCallbacks, |
| 239 | isStrictMode: boolean, |
| 240 | // TODO: Remove `concurrentUpdatesByDefaultOverride`. It is now ignored. |
| 241 | concurrentUpdatesByDefaultOverride: null | boolean, |
| 242 | identifierPrefix: string, |
| 243 | onUncaughtError: ( |
| 244 | error: mixed, |
| 245 | errorInfo: {+componentStack?: ?string}, |
| 246 | ) => void, |
| 247 | onCaughtError: ( |
| 248 | error: mixed, |
| 249 | errorInfo: { |
| 250 | +componentStack?: ?string, |
| 251 | +errorBoundary?: ?component(...props: any), |
| 252 | }, |
| 253 | ) => void, |
| 254 | onRecoverableError: ( |
| 255 | error: mixed, |
| 256 | errorInfo: {+componentStack?: ?string}, |
| 257 | ) => void, |
| 258 | onDefaultTransitionIndicator: () => void | (() => void), |
| 259 | transitionCallbacks: null | TransitionTracingCallbacks, |
| 260 | ): OpaqueRoot { |
| 261 | const hydrate = false; |
| 262 | const initialChildren = null; |
| 263 | const root = createFiberRoot( |
| 264 | containerInfo, |
| 265 | tag, |
| 266 | hydrate, |
| 267 | initialChildren, |
| 268 | hydrationCallbacks, |
| 269 | isStrictMode, |
| 270 | identifierPrefix, |
| 271 | null, |
| 272 | onUncaughtError, |
| 273 | onCaughtError, |
| 274 | onRecoverableError, |
| 275 | onDefaultTransitionIndicator, |
| 276 | transitionCallbacks, |
| 277 | ); |
| 278 | registerDefaultIndicator(onDefaultTransitionIndicator); |
| 279 | return root; |
| 280 | } |
| 281 | |
| 282 | export function createHydrationContainer( |
| 283 | initialChildren: ReactNodeList, |
| 284 | // TODO: Remove `callback` when we delete legacy mode. |
| 285 | callback: ?Function, |
| 286 | containerInfo: Container, |
| 287 | tag: RootTag, |
| 288 | hydrationCallbacks: null | SuspenseHydrationCallbacks, |
| 289 | isStrictMode: boolean, |
| 290 | // TODO: Remove `concurrentUpdatesByDefaultOverride`. It is now ignored. |
| 291 | concurrentUpdatesByDefaultOverride: null | boolean, |
| 292 | identifierPrefix: string, |
| 293 | onUncaughtError: ( |
| 294 | error: mixed, |
| 295 | errorInfo: {+componentStack?: ?string}, |
| 296 | ) => void, |
| 297 | onCaughtError: ( |
| 298 | error: mixed, |
| 299 | errorInfo: { |
| 300 | +componentStack?: ?string, |
| 301 | +errorBoundary?: ?component(...props: any), |
| 302 | }, |
| 303 | ) => void, |
| 304 | onRecoverableError: ( |
| 305 | error: mixed, |
| 306 | errorInfo: {+componentStack?: ?string}, |
| 307 | ) => void, |
| 308 | onDefaultTransitionIndicator: () => void | (() => void), |
| 309 | transitionCallbacks: null | TransitionTracingCallbacks, |
| 310 | formState: ReactFormState<any, any> | null, |
| 311 | ): OpaqueRoot { |
| 312 | const hydrate = true; |
| 313 | const root = createFiberRoot( |
| 314 | containerInfo, |
| 315 | tag, |
| 316 | hydrate, |
| 317 | initialChildren, |
| 318 | hydrationCallbacks, |
| 319 | isStrictMode, |
| 320 | identifierPrefix, |
| 321 | formState, |
| 322 | onUncaughtError, |
| 323 | onCaughtError, |
| 324 | onRecoverableError, |
| 325 | onDefaultTransitionIndicator, |
| 326 | transitionCallbacks, |
| 327 | ); |
| 328 | |
| 329 | registerDefaultIndicator(onDefaultTransitionIndicator); |
| 330 | |
| 331 | // TODO: Move this to FiberRoot constructor |
| 332 | root.context = getContextForSubtree(null); |
| 333 | |
| 334 | // Schedule the initial render. In a hydration root, this is different from |
| 335 | // a regular update because the initial render must match was was rendered |
| 336 | // on the server. |
| 337 | // NOTE: This update intentionally doesn't have a payload. We're only using |
| 338 | // the update to schedule work on the root fiber (and, for legacy roots, to |
| 339 | // enqueue the callback if one is provided). |
| 340 | const current = root.current; |
| 341 | let lane = requestUpdateLane(current); |
| 342 | lane = getBumpedLaneForHydrationByLane(lane); |
| 343 | const update = createUpdate(lane); |
| 344 | update.callback = |
| 345 | callback !== undefined && callback !== null ? callback : null; |
| 346 | enqueueUpdate(current, update, lane); |
| 347 | startUpdateTimerByLane(lane, 'hydrateRoot()', null); |
| 348 | scheduleInitialHydrationOnRoot(root, lane); |
| 349 | |
| 350 | return root; |
| 351 | } |
| 352 | |
| 353 | export function updateContainer( |
| 354 | element: ReactNodeList, |
| 355 | container: OpaqueRoot, |
| 356 | parentComponent: ?component(...props: any), |
| 357 | callback: ?Function, |
| 358 | ): Lane { |
| 359 | const current = container.current; |
| 360 | const lane = requestUpdateLane(current); |
| 361 | updateContainerImpl( |
| 362 | current, |
| 363 | lane, |
| 364 | element, |
| 365 | container, |
| 366 | parentComponent, |
| 367 | callback, |
| 368 | ); |
| 369 | return lane; |
| 370 | } |
| 371 | |
| 372 | export function updateContainerSync( |
| 373 | element: ReactNodeList, |
| 374 | container: OpaqueRoot, |
| 375 | parentComponent: ?component(...props: any), |
| 376 | callback: ?Function, |
| 377 | ): Lane { |
| 378 | if (!disableLegacyMode && container.tag === LegacyRoot) { |
| 379 | flushPendingEffects(); |
| 380 | } |
| 381 | const current = container.current; |
| 382 | updateContainerImpl( |
| 383 | current, |
| 384 | SyncLane, |
| 385 | element, |
| 386 | container, |
| 387 | parentComponent, |
| 388 | callback, |
| 389 | ); |
| 390 | return SyncLane; |
| 391 | } |
| 392 | |
| 393 | function updateContainerImpl( |
| 394 | rootFiber: Fiber, |
| 395 | lane: Lane, |
| 396 | element: ReactNodeList, |
| 397 | container: OpaqueRoot, |
| 398 | parentComponent: ?component(...props: any), |
| 399 | callback: ?Function, |
| 400 | ): void { |
| 401 | if (__DEV__) { |
| 402 | onScheduleRoot(container, element); |
| 403 | } |
| 404 | |
| 405 | if (enableSchedulingProfiler) { |
| 406 | markRenderScheduled(lane); |
| 407 | } |
| 408 | |
| 409 | const context = getContextForSubtree(parentComponent); |
| 410 | if (container.context === null) { |
| 411 | container.context = context; |
| 412 | } else { |
| 413 | container.pendingContext = context; |
| 414 | } |
| 415 | |
| 416 | if (__DEV__) { |
| 417 | if ( |
| 418 | ReactCurrentFiberIsRendering && |
| 419 | ReactCurrentFiberCurrent !== null && |
| 420 | !didWarnAboutNestedUpdates |
| 421 | ) { |
| 422 | didWarnAboutNestedUpdates = true; |
| 423 | console.error( |
| 424 | 'Render methods should be a pure function of props and state; ' + |
| 425 | 'triggering nested component updates from render is not allowed. ' + |
| 426 | 'If necessary, trigger nested updates in componentDidUpdate.\n\n' + |
| 427 | 'Check the render method of %s.', |
| 428 | getComponentNameFromFiber(ReactCurrentFiberCurrent) || 'Unknown', |
| 429 | ); |
| 430 | } |
| 431 | } |
| 432 | |
| 433 | const update = createUpdate(lane); |
| 434 | // Caution: React DevTools currently depends on this property |
| 435 | // being called "element". |
| 436 | update.payload = {element}; |
| 437 | |
| 438 | callback = callback === undefined ? null : callback; |
| 439 | if (callback !== null) { |
| 440 | if (__DEV__) { |
| 441 | if (typeof callback !== 'function') { |
| 442 | console.error( |
| 443 | 'Expected the last optional `callback` argument to be a ' + |
| 444 | 'function. Instead received: %s.', |
| 445 | callback, |
| 446 | ); |
| 447 | } |
| 448 | } |
| 449 | update.callback = callback; |
| 450 | } |
| 451 | |
| 452 | const root = enqueueUpdate(rootFiber, update, lane); |
| 453 | if (root !== null) { |
| 454 | startUpdateTimerByLane(lane, 'root.render()', null); |
| 455 | scheduleUpdateOnFiber(root, rootFiber, lane); |
| 456 | entangleTransitions(root, rootFiber, lane); |
| 457 | } |
| 458 | } |
| 459 | |
| 460 | export { |
| 461 | batchedUpdates, |
| 462 | deferredUpdates, |
| 463 | discreteUpdates, |
| 464 | flushSyncFromReconciler, |
| 465 | flushSyncWork, |
| 466 | isAlreadyRendering, |
| 467 | flushPendingEffects as flushPassiveEffects, |
| 468 | }; |
| 469 | |
| 470 | export function getPublicRootInstance( |
| 471 | container: OpaqueRoot, |
| 472 | ): component(...props: any) | PublicInstance | null { |
| 473 | const containerFiber = container.current; |
| 474 | if (!containerFiber.child) { |
| 475 | return null; |
| 476 | } |
| 477 | switch (containerFiber.child.tag) { |
| 478 | case HostSingleton: |
| 479 | case HostComponent: |
| 480 | return getPublicInstance(containerFiber.child.stateNode); |
| 481 | default: |
| 482 | return containerFiber.child.stateNode; |
| 483 | } |
| 484 | } |
| 485 | |
| 486 | export function attemptSynchronousHydration(fiber: Fiber): void { |
| 487 | switch (fiber.tag) { |
| 488 | case HostRoot: { |
| 489 | const root: FiberRoot = fiber.stateNode; |
| 490 | if (isRootDehydrated(root)) { |
| 491 | // Flush the first scheduled "update". |
| 492 | const lanes = getHighestPriorityPendingLanes(root); |
| 493 | flushRoot(root, lanes); |
| 494 | } |
| 495 | break; |
| 496 | } |
| 497 | case ActivityComponent: |
| 498 | case SuspenseComponent: { |
| 499 | const root = enqueueConcurrentRenderForLane(fiber, SyncLane); |
| 500 | if (root !== null) { |
| 501 | scheduleUpdateOnFiber(root, fiber, SyncLane); |
| 502 | } |
| 503 | flushSyncWork(); |
| 504 | // If we're still blocked after this, we need to increase |
| 505 | // the priority of any promises resolving within this |
| 506 | // boundary so that they next attempt also has higher pri. |
| 507 | const retryLane = SyncLane; |
| 508 | markRetryLaneIfNotHydrated(fiber, retryLane); |
| 509 | break; |
| 510 | } |
| 511 | } |
| 512 | } |
| 513 | |
| 514 | function markRetryLaneImpl(fiber: Fiber, retryLane: Lane) { |
| 515 | const suspenseState: null | SuspenseState | ActivityState = |
| 516 | fiber.memoizedState; |
| 517 | if (suspenseState !== null && suspenseState.dehydrated !== null) { |
| 518 | suspenseState.retryLane = higherPriorityLane( |
| 519 | suspenseState.retryLane, |
| 520 | retryLane, |
| 521 | ); |
| 522 | } |
| 523 | } |
| 524 | |
| 525 | // Increases the priority of thenables when they resolve within this boundary. |
| 526 | function markRetryLaneIfNotHydrated(fiber: Fiber, retryLane: Lane) { |
| 527 | markRetryLaneImpl(fiber, retryLane); |
| 528 | const alternate = fiber.alternate; |
| 529 | if (alternate) { |
| 530 | markRetryLaneImpl(alternate, retryLane); |
| 531 | } |
| 532 | } |
| 533 | |
| 534 | export function attemptContinuousHydration(fiber: Fiber): void { |
| 535 | if (fiber.tag !== SuspenseComponent && fiber.tag !== ActivityComponent) { |
| 536 | // We ignore HostRoots here because we can't increase |
| 537 | // their priority and they should not suspend on I/O, |
| 538 | // since you have to wrap anything that might suspend in |
| 539 | // Suspense. |
| 540 | return; |
| 541 | } |
| 542 | const lane = SelectiveHydrationLane; |
| 543 | const root = enqueueConcurrentRenderForLane(fiber, lane); |
| 544 | if (root !== null) { |
| 545 | scheduleUpdateOnFiber(root, fiber, lane); |
| 546 | } |
| 547 | markRetryLaneIfNotHydrated(fiber, lane); |
| 548 | } |
| 549 | |
| 550 | export function attemptHydrationAtCurrentPriority(fiber: Fiber): void { |
| 551 | if (fiber.tag !== SuspenseComponent && fiber.tag !== ActivityComponent) { |
| 552 | // We ignore HostRoots here because we can't increase |
| 553 | // their priority other than synchronously flush it. |
| 554 | return; |
| 555 | } |
| 556 | let lane = requestUpdateLane(fiber); |
| 557 | lane = getBumpedLaneForHydrationByLane(lane); |
| 558 | const root = enqueueConcurrentRenderForLane(fiber, lane); |
| 559 | if (root !== null) { |
| 560 | scheduleUpdateOnFiber(root, fiber, lane); |
| 561 | } |
| 562 | markRetryLaneIfNotHydrated(fiber, lane); |
| 563 | } |
| 564 | |
| 565 | export {findHostInstance}; |
| 566 | |
| 567 | export {findHostInstanceWithWarning}; |
| 568 | |
| 569 | export function findHostInstanceWithNoPortals( |
| 570 | fiber: Fiber, |
| 571 | ): PublicInstance | null { |
| 572 | const hostFiber = findCurrentHostFiberWithNoPortals(fiber); |
| 573 | if (hostFiber === null) { |
| 574 | return null; |
| 575 | } |
| 576 | return getPublicInstance(hostFiber.stateNode); |
| 577 | } |
| 578 | |
| 579 | let shouldErrorImpl: Fiber => ?boolean = fiber => null; |
| 580 | |
| 581 | export function shouldError(fiber: Fiber): ?boolean { |
| 582 | return shouldErrorImpl(fiber); |
| 583 | } |
| 584 | |
| 585 | let shouldSuspendImpl = (fiber: Fiber) => false; |
| 586 | |
| 587 | export function shouldSuspend(fiber: Fiber): boolean { |
| 588 | return shouldSuspendImpl(fiber); |
| 589 | } |
| 590 | |
| 591 | let overrideHookState = null; |
| 592 | let overrideHookStateDeletePath = null; |
| 593 | let overrideHookStateRenamePath = null; |
| 594 | let overrideProps = null; |
| 595 | let overridePropsDeletePath = null; |
| 596 | let overridePropsRenamePath = null; |
| 597 | let scheduleUpdate = null; |
| 598 | let scheduleRetry = null; |
| 599 | let setErrorHandler = null; |
| 600 | let setSuspenseHandler = null; |
| 601 | |
| 602 | if (__DEV__) { |
| 603 | const copyWithDeleteImpl = ( |
| 604 | obj: Object | Array<any>, |
| 605 | path: Array<string | number>, |
| 606 | index: number, |
| 607 | ): $FlowFixMe => { |
| 608 | const key = path[index]; |
| 609 | const updated = isArray(obj) ? obj.slice() : {...obj}; |
| 610 | if (index + 1 === path.length) { |
| 611 | if (isArray(updated)) { |
| 612 | updated.splice(key as any as number, 1); |
| 613 | } else { |
| 614 | delete updated[key]; |
| 615 | } |
| 616 | return updated; |
| 617 | } |
| 618 | // $FlowFixMe[incompatible-use] number or string is fine here |
| 619 | updated[key] = copyWithDeleteImpl(obj[key], path, index + 1); |
| 620 | return updated; |
| 621 | }; |
| 622 | |
| 623 | const copyWithDelete = ( |
| 624 | obj: Object | Array<any>, |
| 625 | path: Array<string | number>, |
| 626 | ): Object | Array<any> => { |
| 627 | return copyWithDeleteImpl(obj, path, 0); |
| 628 | }; |
| 629 | |
| 630 | const copyWithRenameImpl = ( |
| 631 | obj: Object | Array<any>, |
| 632 | oldPath: Array<string | number>, |
| 633 | newPath: Array<string | number>, |
| 634 | index: number, |
| 635 | ): $FlowFixMe => { |
| 636 | const oldKey = oldPath[index]; |
| 637 | const updated = isArray(obj) ? obj.slice() : {...obj}; |
| 638 | if (index + 1 === oldPath.length) { |
| 639 | const newKey = newPath[index]; |
| 640 | // $FlowFixMe[incompatible-use] number or string is fine here |
| 641 | updated[newKey] = updated[oldKey]; |
| 642 | if (isArray(updated)) { |
| 643 | updated.splice(oldKey as any as number, 1); |
| 644 | } else { |
| 645 | delete updated[oldKey]; |
| 646 | } |
| 647 | } else { |
| 648 | // $FlowFixMe[incompatible-use] number or string is fine here |
| 649 | updated[oldKey] = copyWithRenameImpl( |
| 650 | // $FlowFixMe[incompatible-use] number or string is fine here |
| 651 | obj[oldKey], |
| 652 | oldPath, |
| 653 | newPath, |
| 654 | index + 1, |
| 655 | ); |
| 656 | } |
| 657 | return updated; |
| 658 | }; |
| 659 | |
| 660 | const copyWithRename = ( |
| 661 | obj: Object | Array<any>, |
| 662 | oldPath: Array<string | number>, |
| 663 | newPath: Array<string | number>, |
| 664 | ): Object | Array<any> => { |
| 665 | if (oldPath.length !== newPath.length) { |
| 666 | console.warn('copyWithRename() expects paths of the same length'); |
| 667 | return; |
| 668 | } else { |
| 669 | for (let i = 0; i < newPath.length - 1; i++) { |
| 670 | if (oldPath[i] !== newPath[i]) { |
| 671 | console.warn( |
| 672 | 'copyWithRename() expects paths to be the same except for the deepest key', |
| 673 | ); |
| 674 | return; |
| 675 | } |
| 676 | } |
| 677 | } |
| 678 | return copyWithRenameImpl(obj, oldPath, newPath, 0); |
| 679 | }; |
| 680 | |
| 681 | const copyWithSetImpl = ( |
| 682 | obj: Object | Array<any>, |
| 683 | path: Array<string | number>, |
| 684 | index: number, |
| 685 | value: any, |
| 686 | ): $FlowFixMe => { |
| 687 | if (index >= path.length) { |
| 688 | return value; |
| 689 | } |
| 690 | const key = path[index]; |
| 691 | const updated = isArray(obj) ? obj.slice() : {...obj}; |
| 692 | // $FlowFixMe[incompatible-use] number or string is fine here |
| 693 | updated[key] = copyWithSetImpl(obj[key], path, index + 1, value); |
| 694 | return updated; |
| 695 | }; |
| 696 | |
| 697 | const copyWithSet = ( |
| 698 | obj: Object | Array<any>, |
| 699 | path: Array<string | number>, |
| 700 | value: any, |
| 701 | ): Object | Array<any> => { |
| 702 | return copyWithSetImpl(obj, path, 0, value); |
| 703 | }; |
| 704 | |
| 705 | const findHook = (fiber: Fiber, id: number) => { |
| 706 | // For now, the "id" of stateful hooks is just the stateful hook index. |
| 707 | // This may change in the future with e.g. nested hooks. |
| 708 | let currentHook = fiber.memoizedState; |
| 709 | while (currentHook !== null && id > 0) { |
| 710 | currentHook = currentHook.next; |
| 711 | id--; |
| 712 | } |
| 713 | return currentHook; |
| 714 | }; |
| 715 | |
| 716 | // Support DevTools editable values for useState and useReducer. |
| 717 | overrideHookState = ( |
| 718 | fiber: Fiber, |
| 719 | id: number, |
| 720 | path: Array<string | number>, |
| 721 | value: any, |
| 722 | ) => { |
| 723 | const hook = findHook(fiber, id); |
| 724 | if (hook !== null) { |
| 725 | const newState = copyWithSet(hook.memoizedState, path, value); |
| 726 | hook.memoizedState = newState; |
| 727 | hook.baseState = newState; |
| 728 | |
| 729 | // We aren't actually adding an update to the queue, |
| 730 | // because there is no update we can add for useReducer hooks that won't trigger an error. |
| 731 | // (There's no appropriate action type for DevTools overrides.) |
| 732 | // As a result though, React will see the scheduled update as a noop and bailout. |
| 733 | // Shallow cloning props works as a workaround for now to bypass the bailout check. |
| 734 | fiber.memoizedProps = {...fiber.memoizedProps}; |
| 735 | |
| 736 | const root = enqueueConcurrentRenderForLane(fiber, SyncLane); |
| 737 | if (root !== null) { |
| 738 | scheduleUpdateOnFiber(root, fiber, SyncLane); |
| 739 | } |
| 740 | } |
| 741 | }; |
| 742 | overrideHookStateDeletePath = ( |
| 743 | fiber: Fiber, |
| 744 | id: number, |
| 745 | path: Array<string | number>, |
| 746 | ) => { |
| 747 | const hook = findHook(fiber, id); |
| 748 | if (hook !== null) { |
| 749 | const newState = copyWithDelete(hook.memoizedState, path); |
| 750 | hook.memoizedState = newState; |
| 751 | hook.baseState = newState; |
| 752 | |
| 753 | // We aren't actually adding an update to the queue, |
| 754 | // because there is no update we can add for useReducer hooks that won't trigger an error. |
| 755 | // (There's no appropriate action type for DevTools overrides.) |
| 756 | // As a result though, React will see the scheduled update as a noop and bailout. |
| 757 | // Shallow cloning props works as a workaround for now to bypass the bailout check. |
| 758 | fiber.memoizedProps = {...fiber.memoizedProps}; |
| 759 | |
| 760 | const root = enqueueConcurrentRenderForLane(fiber, SyncLane); |
| 761 | if (root !== null) { |
| 762 | scheduleUpdateOnFiber(root, fiber, SyncLane); |
| 763 | } |
| 764 | } |
| 765 | }; |
| 766 | overrideHookStateRenamePath = ( |
| 767 | fiber: Fiber, |
| 768 | id: number, |
| 769 | oldPath: Array<string | number>, |
| 770 | newPath: Array<string | number>, |
| 771 | ) => { |
| 772 | const hook = findHook(fiber, id); |
| 773 | if (hook !== null) { |
| 774 | const newState = copyWithRename(hook.memoizedState, oldPath, newPath); |
| 775 | hook.memoizedState = newState; |
| 776 | hook.baseState = newState; |
| 777 | |
| 778 | // We aren't actually adding an update to the queue, |
| 779 | // because there is no update we can add for useReducer hooks that won't trigger an error. |
| 780 | // (There's no appropriate action type for DevTools overrides.) |
| 781 | // As a result though, React will see the scheduled update as a noop and bailout. |
| 782 | // Shallow cloning props works as a workaround for now to bypass the bailout check. |
| 783 | fiber.memoizedProps = {...fiber.memoizedProps}; |
| 784 | |
| 785 | const root = enqueueConcurrentRenderForLane(fiber, SyncLane); |
| 786 | if (root !== null) { |
| 787 | scheduleUpdateOnFiber(root, fiber, SyncLane); |
| 788 | } |
| 789 | } |
| 790 | }; |
| 791 | |
| 792 | // Support DevTools props for function components, forwardRef, memo, host components, etc. |
| 793 | overrideProps = (fiber: Fiber, path: Array<string | number>, value: any) => { |
| 794 | fiber.pendingProps = copyWithSet(fiber.memoizedProps, path, value); |
| 795 | if (fiber.alternate) { |
| 796 | fiber.alternate.pendingProps = fiber.pendingProps; |
| 797 | } |
| 798 | const root = enqueueConcurrentRenderForLane(fiber, SyncLane); |
| 799 | if (root !== null) { |
| 800 | scheduleUpdateOnFiber(root, fiber, SyncLane); |
| 801 | } |
| 802 | }; |
| 803 | overridePropsDeletePath = (fiber: Fiber, path: Array<string | number>) => { |
| 804 | fiber.pendingProps = copyWithDelete(fiber.memoizedProps, path); |
| 805 | if (fiber.alternate) { |
| 806 | fiber.alternate.pendingProps = fiber.pendingProps; |
| 807 | } |
| 808 | const root = enqueueConcurrentRenderForLane(fiber, SyncLane); |
| 809 | if (root !== null) { |
| 810 | scheduleUpdateOnFiber(root, fiber, SyncLane); |
| 811 | } |
| 812 | }; |
| 813 | overridePropsRenamePath = ( |
| 814 | fiber: Fiber, |
| 815 | oldPath: Array<string | number>, |
| 816 | newPath: Array<string | number>, |
| 817 | ) => { |
| 818 | fiber.pendingProps = copyWithRename(fiber.memoizedProps, oldPath, newPath); |
| 819 | if (fiber.alternate) { |
| 820 | fiber.alternate.pendingProps = fiber.pendingProps; |
| 821 | } |
| 822 | const root = enqueueConcurrentRenderForLane(fiber, SyncLane); |
| 823 | if (root !== null) { |
| 824 | scheduleUpdateOnFiber(root, fiber, SyncLane); |
| 825 | } |
| 826 | }; |
| 827 | |
| 828 | scheduleUpdate = (fiber: Fiber) => { |
| 829 | const root = enqueueConcurrentRenderForLane(fiber, SyncLane); |
| 830 | if (root !== null) { |
| 831 | scheduleUpdateOnFiber(root, fiber, SyncLane); |
| 832 | } |
| 833 | }; |
| 834 | |
| 835 | scheduleRetry = (fiber: Fiber) => { |
| 836 | const lane = claimNextRetryLane(); |
| 837 | const root = enqueueConcurrentRenderForLane(fiber, lane); |
| 838 | if (root !== null) { |
| 839 | scheduleUpdateOnFiber(root, fiber, lane); |
| 840 | } |
| 841 | }; |
| 842 | |
| 843 | setErrorHandler = (newShouldErrorImpl: Fiber => ?boolean) => { |
| 844 | shouldErrorImpl = newShouldErrorImpl; |
| 845 | }; |
| 846 | |
| 847 | setSuspenseHandler = (newShouldSuspendImpl: Fiber => boolean) => { |
| 848 | shouldSuspendImpl = newShouldSuspendImpl; |
| 849 | }; |
| 850 | } |
| 851 | |
| 852 | function getCurrentFiberForDevTools() { |
| 853 | return ReactCurrentFiberCurrent; |
| 854 | } |
| 855 | |
| 856 | function getLaneLabelMap(): Map<Lane, string> | null { |
| 857 | if (enableSchedulingProfiler) { |
| 858 | const map: Map<Lane, string> = new Map(); |
| 859 | |
| 860 | let lane = 1; |
| 861 | for (let index = 0; index < TotalLanes; index++) { |
| 862 | const label = getLabelForLane(lane) as any as string; |
| 863 | map.set(lane, label); |
| 864 | lane *= 2; |
| 865 | } |
| 866 | |
| 867 | return map; |
| 868 | } else { |
| 869 | return null; |
| 870 | } |
| 871 | } |
| 872 | |
| 873 | export function injectIntoDevTools(): boolean { |
| 874 | const internals: Object = { |
| 875 | bundleType: __DEV__ ? 1 : 0, // Might add PROFILE later. |
| 876 | version: rendererVersion, |
| 877 | rendererPackageName: rendererPackageName, |
| 878 | currentDispatcherRef: ReactSharedInternals, |
| 879 | // Enables DevTools to detect reconciler version rather than renderer version |
| 880 | // which may not match for third party renderers. |
| 881 | reconcilerVersion: ReactVersion, |
| 882 | }; |
| 883 | // $FlowFixMe[invalid-compare] |
| 884 | if (extraDevToolsConfig !== null) { |
| 885 | internals.rendererConfig = extraDevToolsConfig as RendererInspectionConfig; |
| 886 | } |
| 887 | if (__DEV__) { |
| 888 | internals.overrideHookState = overrideHookState; |
| 889 | internals.overrideHookStateDeletePath = overrideHookStateDeletePath; |
| 890 | internals.overrideHookStateRenamePath = overrideHookStateRenamePath; |
| 891 | internals.overrideProps = overrideProps; |
| 892 | internals.overridePropsDeletePath = overridePropsDeletePath; |
| 893 | internals.overridePropsRenamePath = overridePropsRenamePath; |
| 894 | internals.scheduleUpdate = scheduleUpdate; |
| 895 | internals.scheduleRetry = scheduleRetry; |
| 896 | internals.setErrorHandler = setErrorHandler; |
| 897 | internals.setSuspenseHandler = setSuspenseHandler; |
| 898 | // React Refresh |
| 899 | internals.scheduleRefresh = scheduleRefresh; |
| 900 | internals.scheduleRoot = scheduleRoot; |
| 901 | internals.setRefreshHandler = setRefreshHandler; |
| 902 | // Enables DevTools to append owner stacks to error messages in DEV mode. |
| 903 | internals.getCurrentFiber = getCurrentFiberForDevTools; |
| 904 | } |
| 905 | if (enableSchedulingProfiler) { |
| 906 | // Conditionally inject these hooks only if Timeline profiler is supported by this build. |
| 907 | // This gives DevTools a way to feature detect that isn't tied to version number |
| 908 | // (since profiling and timeline are controlled by different feature flags). |
| 909 | internals.getLaneLabelMap = getLaneLabelMap; |
| 910 | internals.injectProfilingHooks = injectProfilingHooks; |
| 911 | } |
| 912 | return injectInternals(internals); |
| 913 | } |