From ca8fc27eb0eef29d60f9ee6689bc65d6b5f1c11e Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Fri, 27 Feb 2026 16:37:13 +0000 Subject: [PATCH] chore: extract pure functions from fiber/renderer.js --- .../src/backend/fiber/renderer.js | 446 ++---------------- .../shared/DevToolsFiberChangeDetection.js | 150 ++++++ .../shared/DevToolsFiberSuspenseUtils.js | 47 ++ .../fiber/shared/DevToolsFiberTypes.js | 99 ++++ .../fiber/shared/DevToolsFiberUtils.js | 158 +++++++ .../src/backend/types.js | 2 +- 6 files changed, 499 insertions(+), 403 deletions(-) create mode 100644 packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberChangeDetection.js create mode 100644 packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberSuspenseUtils.js create mode 100644 packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberTypes.js create mode 100644 packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberUtils.js diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 266346a6bbe7..88160274612d 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -18,7 +18,7 @@ import type { Wakeable, } from 'shared/ReactTypes'; -import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks'; +import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks'; import { ComponentFilterDisplayName, @@ -124,10 +124,30 @@ import {enableStyleXFeatures} from 'react-devtools-feature-flags'; import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponentLogs'; -import is from 'shared/objectIs'; - import {getIODescription} from 'shared/ReactIODescription'; +import { + isError, + getCurrentTime, + getPublicInstance, + getNativeTag, + rootSupportsProfiling, + isErrorBoundary, + getSecondaryEnvironmentName, + areEqualRects, +} from './shared/DevToolsFiberUtils'; +import { + didFiberRender, + getContextChanged, + getChangedHooksIndices, + getChangedKeys, +} from './shared/DevToolsFiberChangeDetection'; +import { + ioExistsInSuspenseAncestor, + getAwaitInSuspendedByFromIO, + getVirtualEndTime, +} from './shared/DevToolsFiberSuspenseUtils'; + import { getStackByFiberInDevAndProd, getOwnerStackByFiberInDev, @@ -135,13 +155,6 @@ import { supportsConsoleTasks, } from './DevToolsFiberComponentStack'; -// $FlowFixMe[method-unbinding] -const toString = Object.prototype.toString; - -function isError(object: mixed) { - return toString.call(object) === '[object Error]'; -} - import {getStyleXData} from '../StyleX/utils'; import {createProfilingHooks} from '../profilingHooks'; @@ -160,6 +173,7 @@ import type { ProfilingDataBackend, ProfilingDataForRootBackend, ReactRenderer, + Rect, RendererInterface, SerializedElement, SerializedAsyncInfo, @@ -175,30 +189,21 @@ import type { Plugins, } from 'react-devtools-shared/src/frontend/types'; import type {ReactFunctionLocation} from 'shared/ReactTypes'; +import type { + FiberInstance, + FilteredFiberInstance, + VirtualInstance, + DevToolsInstance, + SuspenseNode, +} from './shared/DevToolsFiberTypes'; +import { + FIBER_INSTANCE, + VIRTUAL_INSTANCE, + FILTERED_FIBER_INSTANCE, +} from './shared/DevToolsFiberTypes'; import {getSourceLocationByFiber} from './DevToolsFiberComponentStack'; import {formatOwnerStack} from '../shared/DevToolsOwnerStack'; -// Kinds -const FIBER_INSTANCE = 0; -const VIRTUAL_INSTANCE = 1; -const FILTERED_FIBER_INSTANCE = 2; - -// This type represents a stateful instance of a Client Component i.e. a Fiber pair. -// These instances also let us track stateful DevTools meta data like id and warnings. -type FiberInstance = { - kind: 0, - id: number, - parent: null | DevToolsInstance, - firstChild: null | DevToolsInstance, - nextSibling: null | DevToolsInstance, - source: null | string | Error | ReactFunctionLocation, // source location of this component function, or owned child stack - logCount: number, // total number of errors/warnings last seen - treeBaseDuration: number, // the profiled time of the last render of this subtree - suspendedBy: null | Array, // things that suspended in the children position of this component - suspenseNode: null | SuspenseNode, - data: Fiber, // one of a Fiber pair -}; - function createFiberInstance(fiber: Fiber): FiberInstance { return { kind: FIBER_INSTANCE, @@ -215,22 +220,6 @@ function createFiberInstance(fiber: Fiber): FiberInstance { }; } -type FilteredFiberInstance = { - kind: 2, - // We exclude id from the type to get errors if we try to access it. - // However it is still in the object to preserve hidden class. - // id: number, - parent: null | DevToolsInstance, - firstChild: null | DevToolsInstance, - nextSibling: null | DevToolsInstance, - source: null | string | Error | ReactFunctionLocation, // always null here. - logCount: number, // total number of errors/warnings last seen - treeBaseDuration: number, // the profiled time of the last render of this subtree - suspendedBy: null | Array, // only used at the root - suspenseNode: null | SuspenseNode, - data: Fiber, // one of a Fiber pair -}; - // This is used to represent a filtered Fiber but still lets us find its host instance. function createFilteredFiberInstance(fiber: Fiber): FilteredFiberInstance { return ({ @@ -248,27 +237,6 @@ function createFilteredFiberInstance(fiber: Fiber): FilteredFiberInstance { }: any); } -// This type represents a stateful instance of a Server Component or a Component -// that gets optimized away - e.g. call-through without creating a Fiber. -// It's basically a virtual Fiber. This is not a semantic concept in React. -// It only exists as a virtual concept to let the same Element in the DevTools -// persist. To be selectable separately from all ReactComponentInfo and overtime. -type VirtualInstance = { - kind: 1, - id: number, - parent: null | DevToolsInstance, - firstChild: null | DevToolsInstance, - nextSibling: null | DevToolsInstance, - source: null | string | Error | ReactFunctionLocation, // source location of this server component, or owned child stack - logCount: number, // total number of errors/warnings last seen - treeBaseDuration: number, // the profiled time of the last render of this subtree - suspendedBy: null | Array, // things that blocked the server component's child from rendering - suspenseNode: null, - // The latest info for this instance. This can be updated over time and the - // same info can appear in more than once ServerComponentInstance. - data: ReactComponentInfo, -}; - function createVirtualInstance( debugEntry: ReactComponentInfo, ): VirtualInstance { @@ -287,30 +255,6 @@ function createVirtualInstance( }; } -type DevToolsInstance = FiberInstance | VirtualInstance | FilteredFiberInstance; - -// A Generic Rect super type which can include DOMRect and other objects with similar shape like in React Native. -type Rect = {x: number, y: number, width: number, height: number, ...}; - -type SuspenseNode = { - // The Instance can be a Suspense boundary, a SuspenseList Row, or HostRoot. - // It can also be disconnected from the main tree if it's a Filtered Instance. - instance: FiberInstance | FilteredFiberInstance, - parent: null | SuspenseNode, - firstChild: null | SuspenseNode, - nextSibling: null | SuspenseNode, - rects: null | Array, // The bounding rects of content children. - suspendedBy: Map>, // Tracks which data we're suspended by and the children that suspend it. - environments: Map, // Tracks the Flight environment names that suspended this. I.e. if the server blocked this. - endTime: number, // Track a short cut to the maximum end time value within the suspendedBy set. - // Track whether any of the items in suspendedBy are unique this this Suspense boundaries or if they're all - // also in the parent sets. This determine whether this could contribute in the loading sequence. - hasUniqueSuspenders: boolean, - // Track whether anything suspended in this boundary that we can't track either because it was using throw - // a promise, an older version of React or because we're inspecting prod. - hasUnknownSuspenders: boolean, -}; - // Update flags need to be propagated up until the caller that put the corresponding // node on the stack. // If you push a new node, you need to handle ShouldResetChildren when you pop it. @@ -375,18 +319,6 @@ export function getDispatcherRef(renderer: { return (injectedRef: any); } -function getFiberFlags(fiber: Fiber): number { - // The name of this field changed from "effectTag" to "flags" - return fiber.flags !== undefined ? fiber.flags : (fiber: any).effectTag; -} - -// Some environments (e.g. React Native / Hermes) don't support the performance API yet. -const getCurrentTime = - // $FlowFixMe[method-unbinding] - typeof performance === 'object' && typeof performance.now === 'function' - ? () => performance.now() - : () => Date.now(); - export function getInternalReactConstants(version: string): { getDisplayNameForFiber: getDisplayNameForFiberType, getTypeSymbol: getTypeSymbolType, @@ -883,53 +815,6 @@ const hostResourceToDevToolsInstanceMap: Map< Set, > = new Map(); -// Ideally, this should be injected from Reconciler config -function getPublicInstance(instance: HostInstance): HostInstance { - // Typically the PublicInstance and HostInstance is the same thing but not in Fabric. - // So we need to detect this and use that as the public instance. - - // React Native. Modern. Fabric. - if (typeof instance === 'object' && instance !== null) { - if (typeof instance.canonical === 'object' && instance.canonical !== null) { - if ( - typeof instance.canonical.publicInstance === 'object' && - instance.canonical.publicInstance !== null - ) { - return instance.canonical.publicInstance; - } - } - - // React Native. Legacy. Paper. - if (typeof instance._nativeTag === 'number') { - return instance._nativeTag; - } - } - - // React Web. Usually a DOM element. - return instance; -} - -function getNativeTag(instance: HostInstance): number | null { - if (typeof instance !== 'object' || instance === null) { - return null; - } - - // Modern. Fabric. - if ( - instance.canonical != null && - typeof instance.canonical.nativeTag === 'number' - ) { - return instance.canonical.nativeTag; - } - - // Legacy. Paper. - if (typeof instance._nativeTag === 'number') { - return instance._nativeTag; - } - - return null; -} - function aquireHostInstance( nearestInstance: DevToolsInstance, hostInstance: HostInstance, @@ -1029,7 +914,6 @@ export function attach( const { ActivityComponent, ClassComponent, - ContextConsumer, DehydratedSuspenseComponent, ForwardRef, Fragment, @@ -1992,134 +1876,6 @@ export function attach( } } - function getContextChanged(prevFiber: Fiber, nextFiber: Fiber): boolean { - let prevContext = - prevFiber.dependencies && prevFiber.dependencies.firstContext; - let nextContext = - nextFiber.dependencies && nextFiber.dependencies.firstContext; - - while (prevContext && nextContext) { - // Note this only works for versions of React that support this key (e.v. 18+) - // For older versions, there's no good way to read the current context value after render has completed. - // This is because React maintains a stack of context values during render, - // but by the time DevTools is called, render has finished and the stack is empty. - if (prevContext.context !== nextContext.context) { - // If the order of context has changed, then the later context values might have - // changed too but the main reason it rerendered was earlier. Either an earlier - // context changed value but then we would have exited already. If we end up here - // it's because a state or props change caused the order of contexts used to change. - // So the main cause is not the contexts themselves. - return false; - } - if (!is(prevContext.memoizedValue, nextContext.memoizedValue)) { - return true; - } - - prevContext = prevContext.next; - nextContext = nextContext.next; - } - return false; - } - - function didStatefulHookChange(prev: HooksNode, next: HooksNode): boolean { - // Detect the shape of useState() / useReducer() / useTransition() / useSyncExternalStore() / useActionState() - const isStatefulHook = - prev.isStateEditable === true || - prev.name === 'SyncExternalStore' || - prev.name === 'Transition' || - prev.name === 'ActionState' || - prev.name === 'FormState'; - - // Compare the values to see if they changed - if (isStatefulHook) { - return prev.value !== next.value; - } - - return false; - } - - function getChangedHooksIndices( - prevHooks: HooksTree | null, - nextHooks: HooksTree | null, - ): null | Array { - if (prevHooks == null || nextHooks == null) { - return null; - } - - const indices: Array = []; - let index = 0; - - function traverse(prevTree: HooksTree, nextTree: HooksTree): void { - for (let i = 0; i < prevTree.length; i++) { - const prevHook = prevTree[i]; - const nextHook = nextTree[i]; - - if (prevHook.subHooks.length > 0 && nextHook.subHooks.length > 0) { - traverse(prevHook.subHooks, nextHook.subHooks); - continue; - } - - if (didStatefulHookChange(prevHook, nextHook)) { - indices.push(index); - } - - index++; - } - } - - traverse(prevHooks, nextHooks); - return indices; - } - - function getChangedKeys(prev: any, next: any): null | Array { - if (prev == null || next == null) { - return null; - } - - const keys = new Set([...Object.keys(prev), ...Object.keys(next)]); - const changedKeys = []; - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const key of keys) { - if (prev[key] !== next[key]) { - changedKeys.push(key); - } - } - - return changedKeys; - } - - /** - * Returns true iff nextFiber actually performed any work and produced an update. - * For generic components, like Function or Class components, prevFiber is not considered. - */ - function didFiberRender(prevFiber: Fiber, nextFiber: Fiber): boolean { - switch (nextFiber.tag) { - case ClassComponent: - case FunctionComponent: - case ContextConsumer: - case MemoComponent: - case SimpleMemoComponent: - case ForwardRef: - // For types that execute user code, we check PerformedWork effect. - // We don't reflect bailouts (either referential or sCU) in DevTools. - // TODO: This flag is a leaked implementation detail. Once we start - // releasing DevTools in lockstep with React, we should import a - // function from the reconciler instead. - const PerformedWork = 0b000000000000000000000000001; - return (getFiberFlags(nextFiber) & PerformedWork) === PerformedWork; - // Note: ContextConsumer only gets PerformedWork effect in 16.3.3+ - // so it won't get highlighted with React 16.3.0 to 16.3.2. - default: - // For host components and other types, we compare inputs - // to determine whether something is an update. - return ( - prevFiber.memoizedProps !== nextFiber.memoizedProps || - prevFiber.memoizedState !== nextFiber.memoizedState || - prevFiber.ref !== nextFiber.ref - ); - } - } - type OperationsArray = Array; type StringTableEntry = { @@ -2943,20 +2699,6 @@ export function attach( // the current parent here as well. let reconcilingParentSuspenseNode: null | SuspenseNode = null; - function ioExistsInSuspenseAncestor( - suspenseNode: SuspenseNode, - ioInfo: ReactIOInfo, - ): boolean { - let ancestor = suspenseNode.parent; - while (ancestor !== null) { - if (ancestor.suspendedBy.has(ioInfo)) { - return true; - } - ancestor = ancestor.parent; - } - return false; - } - function insertSuspendedBy(asyncInfo: ReactAsyncInfo): void { if (reconcilingParent === null || reconcilingParentSuspenseNode === null) { throw new Error( @@ -3055,19 +2797,6 @@ export function attach( } } - function getAwaitInSuspendedByFromIO( - suspensedBy: Array, - ioInfo: ReactIOInfo, - ): null | ReactAsyncInfo { - for (let i = 0; i < suspensedBy.length; i++) { - const asyncInfo = suspensedBy[i]; - if (asyncInfo.awaited === ioInfo) { - return asyncInfo; - } - } - return null; - } - function unblockSuspendedBy( parentSuspenseNode: SuspenseNode, ioInfo: ReactIOInfo, @@ -3101,15 +2830,6 @@ export function attach( } } - function getVirtualEndTime(ioInfo: ReactIOInfo): number { - if (ioInfo.env != null) { - // Sort client side content first so that scripts and streams don't - // cover up the effect of server time. - return ioInfo.end + 1000000; - } - return ioInfo.end; - } - function computeEndTime(suspenseNode: SuspenseNode) { let maxEndTime = 0; suspenseNode.suspendedBy.forEach((set, ioInfo) => { @@ -3432,34 +3152,6 @@ export function attach( return false; } - function areEqualRects( - a: null | Array, - b: null | Array, - ): boolean { - if (a === null) { - return b === null; - } - if (b === null) { - return false; - } - if (a.length !== b.length) { - return false; - } - for (let i = 0; i < a.length; i++) { - const aRect = a[i]; - const bRect = b[i]; - if ( - aRect.x !== bRect.x || - aRect.y !== bRect.y || - aRect.width !== bRect.width || - aRect.height !== bRect.height - ) { - return false; - } - } - return true; - } - function measureUnchangedSuspenseNodesRecursively( suspenseNode: SuspenseNode, ): void { @@ -3632,25 +3324,6 @@ export function attach( pendingRealUnmountedIDs.push(id); } - function getSecondaryEnvironmentName( - debugInfo: ?ReactDebugInfo, - index: number, - ): null | string { - if (debugInfo != null) { - const componentInfo: ReactComponentInfo = (debugInfo[index]: any); - for (let i = index + 1; i < debugInfo.length; i++) { - const debugEntry = debugInfo[i]; - if (typeof debugEntry.env === 'string') { - // If the next environment is different then this component was the boundary - // and it changed before entering the next component. So we assign this - // component a secondary environment. - return componentInfo.env !== debugEntry.env ? debugEntry.env : null; - } - } - } - return null; - } - function trackDebugInfoFromLazyType(fiber: Fiber): void { // The debugInfo from a Lazy isn't propagated onto _debugInfo of the parent Fiber the way // it is when used in child position. So we need to pick it up explicitly. @@ -4532,7 +4205,8 @@ export function attach( if ( prevFiber == null || - (prevFiber !== fiber && didFiberRender(prevFiber, fiber)) + (prevFiber !== fiber && + didFiberRender(ReactTypeOfWork, prevFiber, fiber)) ) { if (actualDuration != null) { // The actual duration reported by React includes time spent working on children. @@ -5166,6 +4840,7 @@ export function attach( if (prevFiber !== nextFiber) { // Otherwise if this is a traced ancestor, flag for the nearest host descendant(s). traceNearestHostComponentUpdate = didFiberRender( + ReactTypeOfWork, prevFiber, nextFiber, ); @@ -5197,7 +4872,7 @@ export function attach( // Invalidating any Root invalidates the Screen too. (mostRecentlyInspectedElement.type === ElementTypeRoot && nextFiber.tag === HostRoot)) && - didFiberRender(prevFiber, nextFiber) + didFiberRender(ReactTypeOfWork, prevFiber, nextFiber) ) { // If this Fiber has updated, clear cached inspected data. // If it is inspected again, it may need to be re-run to obtain updated hooks values. @@ -5720,22 +5395,6 @@ export function attach( isProfiling = false; } - function rootSupportsProfiling(root: any) { - if (root.memoizedInteractions != null) { - // v16 builds include this field for the scheduler/tracing API. - return true; - } else if ( - root.current != null && - root.current.hasOwnProperty('treeBaseDuration') - ) { - // The scheduler/tracing API was removed in v17 though - // so we need to check a non-root Fiber. - return true; - } else { - return false; - } - } - function flushInitialOperations() { const localPendingOperationsQueue = pendingOperationsQueue; @@ -6804,23 +6463,6 @@ export function attach( return {instance, style}; } - function isErrorBoundary(fiber: Fiber): boolean { - const {tag, type} = fiber; - - switch (tag) { - case ClassComponent: - case IncompleteClassComponent: - const instance = fiber.stateNode; - return ( - typeof type.getDerivedStateFromError === 'function' || - (instance !== null && - typeof instance.componentDidCatch === 'function') - ); - default: - return false; - } - } - function inspectElementRaw(id: number): InspectedElement | null { const devtoolsInstance = idToDevToolsInstanceMap.get(id); if (devtoolsInstance === undefined) { @@ -6995,7 +6637,7 @@ export function attach( current = current.return; if (temp.tag === SuspenseComponent) { hasSuspenseBoundary = true; - } else if (isErrorBoundary(temp)) { + } else if (isErrorBoundary(ReactTypeOfWork, temp)) { hasErrorBoundary = true; } } @@ -7005,7 +6647,7 @@ export function attach( } let isErrored = false; - if (isErrorBoundary(fiber)) { + if (isErrorBoundary(ReactTypeOfWork, fiber)) { // if the current inspected element is an error boundary, // either that we want to use it to toggle off error state // or that we allow to force error state on it if it's within another @@ -7197,7 +6839,7 @@ export function attach( current = current.return; if (temp.tag === SuspenseComponent) { hasSuspenseBoundary = true; - } else if (isErrorBoundary(temp)) { + } else if (isErrorBoundary(ReactTypeOfWork, temp)) { hasErrorBoundary = true; } } @@ -8314,7 +7956,7 @@ export function attach( return; } let fiber = nearestFiber; - while (!isErrorBoundary(fiber)) { + while (!isErrorBoundary(ReactTypeOfWork, fiber)) { if (fiber.return === null) { return; } diff --git a/packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberChangeDetection.js b/packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberChangeDetection.js new file mode 100644 index 000000000000..df73c8bdf7ae --- /dev/null +++ b/packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberChangeDetection.js @@ -0,0 +1,150 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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 {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks'; +import type {WorkTagMap} from '../../types'; + +import {getFiberFlags} from './DevToolsFiberUtils'; +import is from 'shared/objectIs'; + +export function getContextChanged(prevFiber: Fiber, nextFiber: Fiber): boolean { + let prevContext = + prevFiber.dependencies && prevFiber.dependencies.firstContext; + let nextContext = + nextFiber.dependencies && nextFiber.dependencies.firstContext; + + while (prevContext && nextContext) { + // Note this only works for versions of React that support this key (e.v. 18+) + // For older versions, there's no good way to read the current context value after render has completed. + // This is because React maintains a stack of context values during render, + // but by the time DevTools is called, render has finished and the stack is empty. + if (prevContext.context !== nextContext.context) { + // If the order of context has changed, then the later context values might have + // changed too but the main reason it rerendered was earlier. Either an earlier + // context changed value but then we would have exited already. If we end up here + // it's because a state or props change caused the order of contexts used to change. + // So the main cause is not the contexts themselves. + return false; + } + if (!is(prevContext.memoizedValue, nextContext.memoizedValue)) { + return true; + } + + prevContext = prevContext.next; + nextContext = nextContext.next; + } + return false; +} + +export function didStatefulHookChange( + prev: HooksNode, + next: HooksNode, +): boolean { + // Detect the shape of useState() / useReducer() / useTransition() / useSyncExternalStore() / useActionState() + const isStatefulHook = + prev.isStateEditable === true || + prev.name === 'SyncExternalStore' || + prev.name === 'Transition' || + prev.name === 'ActionState' || + prev.name === 'FormState'; + + // Compare the values to see if they changed + if (isStatefulHook) { + return prev.value !== next.value; + } + + return false; +} + +export function getChangedHooksIndices( + prevHooks: HooksTree | null, + nextHooks: HooksTree | null, +): null | Array { + if (prevHooks == null || nextHooks == null) { + return null; + } + + const indices: Array = []; + let index = 0; + + function traverse(prevTree: HooksTree, nextTree: HooksTree): void { + for (let i = 0; i < prevTree.length; i++) { + const prevHook = prevTree[i]; + const nextHook = nextTree[i]; + + if (prevHook.subHooks.length > 0 && nextHook.subHooks.length > 0) { + traverse(prevHook.subHooks, nextHook.subHooks); + continue; + } + + if (didStatefulHookChange(prevHook, nextHook)) { + indices.push(index); + } + + index++; + } + } + + traverse(prevHooks, nextHooks); + return indices; +} + +export function getChangedKeys(prev: any, next: any): null | Array { + if (prev == null || next == null) { + return null; + } + + const keys = new Set([...Object.keys(prev), ...Object.keys(next)]); + const changedKeys = []; + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const key of keys) { + if (prev[key] !== next[key]) { + changedKeys.push(key); + } + } + + return changedKeys; +} + +/** + * Returns true iff nextFiber actually performed any work and produced an update. + * For generic components, like Function or Class components, prevFiber is not considered. + */ +export function didFiberRender( + workTagMap: WorkTagMap, + prevFiber: Fiber, + nextFiber: Fiber, +): boolean { + switch (nextFiber.tag) { + case workTagMap.ClassComponent: + case workTagMap.FunctionComponent: + case workTagMap.ContextConsumer: + case workTagMap.MemoComponent: + case workTagMap.SimpleMemoComponent: + case workTagMap.ForwardRef: + // For types that execute user code, we check PerformedWork effect. + // We don't reflect bailouts (either referential or sCU) in DevTools. + // TODO: This flag is a leaked implementation detail. Once we start + // releasing DevTools in lockstep with React, we should import a + // function from the reconciler instead. + const PerformedWork = 0b000000000000000000000000001; + return (getFiberFlags(nextFiber) & PerformedWork) === PerformedWork; + // Note: ContextConsumer only gets PerformedWork effect in 16.3.3+ + // so it won't get highlighted with React 16.3.0 to 16.3.2. + default: + // For host components and other types, we compare inputs + // to determine whether something is an update. + return ( + prevFiber.memoizedProps !== nextFiber.memoizedProps || + prevFiber.memoizedState !== nextFiber.memoizedState || + prevFiber.ref !== nextFiber.ref + ); + } +} diff --git a/packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberSuspenseUtils.js b/packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberSuspenseUtils.js new file mode 100644 index 000000000000..b0b64b184c22 --- /dev/null +++ b/packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberSuspenseUtils.js @@ -0,0 +1,47 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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 {ReactIOInfo, ReactAsyncInfo} from 'shared/ReactTypes'; +import type {SuspenseNode} from './DevToolsFiberTypes'; + +export function ioExistsInSuspenseAncestor( + suspenseNode: SuspenseNode, + ioInfo: ReactIOInfo, +): boolean { + let ancestor = suspenseNode.parent; + while (ancestor !== null) { + if (ancestor.suspendedBy.has(ioInfo)) { + return true; + } + ancestor = ancestor.parent; + } + return false; +} + +export function getAwaitInSuspendedByFromIO( + suspensedBy: Array, + ioInfo: ReactIOInfo, +): null | ReactAsyncInfo { + for (let i = 0; i < suspensedBy.length; i++) { + const asyncInfo = suspensedBy[i]; + if (asyncInfo.awaited === ioInfo) { + return asyncInfo; + } + } + return null; +} + +export function getVirtualEndTime(ioInfo: ReactIOInfo): number { + if (ioInfo.env != null) { + // Sort client side content first so that scripts and streams don't + // cover up the effect of server time. + return ioInfo.end + 1000000; + } + return ioInfo.end; +} diff --git a/packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberTypes.js b/packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberTypes.js new file mode 100644 index 000000000000..1b01dfbc5bbc --- /dev/null +++ b/packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberTypes.js @@ -0,0 +1,99 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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 { + ReactComponentInfo, + ReactAsyncInfo, + ReactIOInfo, + ReactFunctionLocation, +} from 'shared/ReactTypes'; +import type {Rect} from '../../types'; + +// Kinds +export const FIBER_INSTANCE = 0; +export const VIRTUAL_INSTANCE = 1; +export const FILTERED_FIBER_INSTANCE = 2; + +// This type represents a stateful instance of a Client Component i.e. a Fiber pair. +// These instances also let us track stateful DevTools meta data like id and warnings. +export type FiberInstance = { + kind: 0, + id: number, + parent: null | DevToolsInstance, + firstChild: null | DevToolsInstance, + nextSibling: null | DevToolsInstance, + source: null | string | Error | ReactFunctionLocation, // source location of this component function, or owned child stack + logCount: number, // total number of errors/warnings last seen + treeBaseDuration: number, // the profiled time of the last render of this subtree + suspendedBy: null | Array, // things that suspended in the children position of this component + suspenseNode: null | SuspenseNode, + data: Fiber, // one of a Fiber pair +}; + +export type FilteredFiberInstance = { + kind: 2, + // We exclude id from the type to get errors if we try to access it. + // However it is still in the object to preserve hidden class. + // id: number, + parent: null | DevToolsInstance, + firstChild: null | DevToolsInstance, + nextSibling: null | DevToolsInstance, + source: null | string | Error | ReactFunctionLocation, // always null here. + logCount: number, // total number of errors/warnings last seen + treeBaseDuration: number, // the profiled time of the last render of this subtree + suspendedBy: null | Array, // only used at the root + suspenseNode: null | SuspenseNode, + data: Fiber, // one of a Fiber pair +}; + +// This type represents a stateful instance of a Server Component or a Component +// that gets optimized away - e.g. call-through without creating a Fiber. +// It's basically a virtual Fiber. This is not a semantic concept in React. +// It only exists as a virtual concept to let the same Element in the DevTools +// persist. To be selectable separately from all ReactComponentInfo and overtime. +export type VirtualInstance = { + kind: 1, + id: number, + parent: null | DevToolsInstance, + firstChild: null | DevToolsInstance, + nextSibling: null | DevToolsInstance, + source: null | string | Error | ReactFunctionLocation, // source location of this server component, or owned child stack + logCount: number, // total number of errors/warnings last seen + treeBaseDuration: number, // the profiled time of the last render of this subtree + suspendedBy: null | Array, // things that blocked the server component's child from rendering + suspenseNode: null, + // The latest info for this instance. This can be updated over time and the + // same info can appear in more than once ServerComponentInstance. + data: ReactComponentInfo, +}; + +export type DevToolsInstance = + | FiberInstance + | VirtualInstance + | FilteredFiberInstance; + +export type SuspenseNode = { + // The Instance can be a Suspense boundary, a SuspenseList Row, or HostRoot. + // It can also be disconnected from the main tree if it's a Filtered Instance. + instance: FiberInstance | FilteredFiberInstance, + parent: null | SuspenseNode, + firstChild: null | SuspenseNode, + nextSibling: null | SuspenseNode, + rects: null | Array, // The bounding rects of content children. + suspendedBy: Map>, // Tracks which data we're suspended by and the children that suspend it. + environments: Map, // Tracks the Flight environment names that suspended this. I.e. if the server blocked this. + endTime: number, // Track a short cut to the maximum end time value within the suspendedBy set. + // Track whether any of the items in suspendedBy are unique this this Suspense boundaries or if they're all + // also in the parent sets. This determine whether this could contribute in the loading sequence. + hasUniqueSuspenders: boolean, + // Track whether anything suspended in this boundary that we can't track either because it was using throw + // a promise, an older version of React or because we're inspecting prod. + hasUnknownSuspenders: boolean, +}; diff --git a/packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberUtils.js b/packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberUtils.js new file mode 100644 index 000000000000..d5342202b291 --- /dev/null +++ b/packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberUtils.js @@ -0,0 +1,158 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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 {ReactComponentInfo, ReactDebugInfo} from 'shared/ReactTypes'; +import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; +import type {HostInstance, WorkTagMap} from '../../types'; +import type {Rect} from '../../types'; + +// $FlowFixMe[method-unbinding] +const toString = Object.prototype.toString; + +export function isError(object: mixed): boolean { + return toString.call(object) === '[object Error]'; +} + +export function getFiberFlags(fiber: Fiber): number { + // The name of this field changed from "effectTag" to "flags" + return fiber.flags !== undefined ? fiber.flags : (fiber: any).effectTag; +} + +// Some environments (e.g. React Native / Hermes) don't support the performance API yet. +export const getCurrentTime: () => number = + // $FlowFixMe[method-unbinding] + typeof performance === 'object' && typeof performance.now === 'function' + ? () => performance.now() + : () => Date.now(); + +// Ideally, this should be injected from Reconciler config +export function getPublicInstance(instance: HostInstance): HostInstance { + // Typically the PublicInstance and HostInstance is the same thing but not in Fabric. + // So we need to detect this and use that as the public instance. + + // React Native. Modern. Fabric. + if (typeof instance === 'object' && instance !== null) { + if (typeof instance.canonical === 'object' && instance.canonical !== null) { + if ( + typeof instance.canonical.publicInstance === 'object' && + instance.canonical.publicInstance !== null + ) { + return instance.canonical.publicInstance; + } + } + + // React Native. Legacy. Paper. + if (typeof instance._nativeTag === 'number') { + return instance._nativeTag; + } + } + + // React Web. Usually a DOM element. + return instance; +} + +export function getNativeTag(instance: HostInstance): number | null { + if (typeof instance !== 'object' || instance === null) { + return null; + } + + // Modern. Fabric. + if ( + instance.canonical != null && + typeof instance.canonical.nativeTag === 'number' + ) { + return instance.canonical.nativeTag; + } + + // Legacy. Paper. + if (typeof instance._nativeTag === 'number') { + return instance._nativeTag; + } + + return null; +} + +export function rootSupportsProfiling(root: any): boolean { + if (root.memoizedInteractions != null) { + // v16 builds include this field for the scheduler/tracing API. + return true; + } else if ( + root.current != null && + root.current.hasOwnProperty('treeBaseDuration') + ) { + // The scheduler/tracing API was removed in v17 though + // so we need to check a non-root Fiber. + return true; + } else { + return false; + } +} + +export function isErrorBoundary(workTagMap: WorkTagMap, fiber: Fiber): boolean { + const {tag, type} = fiber; + + switch (tag) { + case workTagMap.ClassComponent: + case workTagMap.IncompleteClassComponent: + const instance = fiber.stateNode; + return ( + typeof type.getDerivedStateFromError === 'function' || + (instance !== null && typeof instance.componentDidCatch === 'function') + ); + default: + return false; + } +} + +export function getSecondaryEnvironmentName( + debugInfo: ?ReactDebugInfo, + index: number, +): null | string { + if (debugInfo != null) { + const componentInfo: ReactComponentInfo = (debugInfo[index]: any); + for (let i = index + 1; i < debugInfo.length; i++) { + const debugEntry = debugInfo[i]; + if (typeof debugEntry.env === 'string') { + // If the next environment is different then this component was the boundary + // and it changed before entering the next component. So we assign this + // component a secondary environment. + return componentInfo.env !== debugEntry.env ? debugEntry.env : null; + } + } + } + return null; +} + +export function areEqualRects( + a: null | Array, + b: null | Array, +): boolean { + if (a === null) { + return b === null; + } + if (b === null) { + return false; + } + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + const aRect = a[i]; + const bRect = b[i]; + if ( + aRect.x !== bRect.x || + aRect.y !== bRect.y || + aRect.width !== bRect.width || + aRect.height !== bRect.height + ) { + return false; + } + } + return true; +} diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index a97439b7cf98..c68601f9b633 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -101,7 +101,7 @@ export type FindHostInstancesForElementID = ( id: number, ) => null | $ReadOnlyArray; -type Rect = { +export type Rect = { x: number, y: number, width: number,