@samitouri / QOS-React-2 / commits / aac12ce597

[DevTools] chore: extract pure functions from fiber/renderer.js (#35924)

I am in a process of splitting down the renderer implementation into smaller units of logic that can be reused. This change is about extracting pure functions only.

Ruslan Lesiutin committed Mar 3, 2026 at 12:27 UTC aac12ce597b49093a5add54b00deee3d8980f874
7 files changed +511 -403
packages/react-devtools-shared/src/backend/DevToolsNativeHost.js new
+64
@@ -0,0 +1,64 @@
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 {HostInstance} from './types';
11 +
12 +// Some environments (e.g. React Native / Hermes) don't support the performance API yet.
13 +export const getCurrentTime: () => number =
14 + // $FlowFixMe[method-unbinding]
15 + typeof performance === 'object' && typeof performance.now === 'function'
16 + ? () => performance.now()
17 + : () => Date.now();
18 +
19 +// Ideally, this should be injected from Reconciler config
20 +export function getPublicInstance(instance: HostInstance): HostInstance {
21 + // Typically the PublicInstance and HostInstance is the same thing but not in Fabric.
22 + // So we need to detect this and use that as the public instance.
23 +
24 + // React Native. Modern. Fabric.
25 + if (typeof instance === 'object' && instance !== null) {
26 + if (typeof instance.canonical === 'object' && instance.canonical !== null) {
27 + if (
28 + typeof instance.canonical.publicInstance === 'object' &&
29 + instance.canonical.publicInstance !== null
30 + ) {
31 + return instance.canonical.publicInstance;
32 + }
33 + }
34 +
35 + // React Native. Legacy. Paper.
36 + if (typeof instance._nativeTag === 'number') {
37 + return instance._nativeTag;
38 + }
39 + }
40 +
41 + // React Web. Usually a DOM element.
42 + return instance;
43 +}
44 +
45 +export function getNativeTag(instance: HostInstance): number | null {
46 + if (typeof instance !== 'object' || instance === null) {
47 + return null;
48 + }
49 +
50 + // Modern. Fabric.
51 + if (
52 + instance.canonical != null &&
53 + typeof instance.canonical.nativeTag === 'number'
54 + ) {
55 + return instance.canonical.nativeTag;
56 + }
57 +
58 + // Legacy. Paper.
59 + if (typeof instance._nativeTag === 'number') {
60 + return instance._nativeTag;
61 + }
62 +
63 + return null;
64 +}
packages/react-devtools-shared/src/backend/fiber/renderer.js
+46 -402
@@ -18,7 +18,7 @@ import type {
18 Wakeable,
19 } from 'shared/ReactTypes';
20
21 -import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
21 +import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
22
23 import {
24 ComponentFilterDisplayName,
@@ -124,10 +124,32 @@ import {enableStyleXFeatures} from 'react-devtools-feature-flags';
124
125 import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponentLogs';
126
127 -import is from 'shared/objectIs';
128 -
127 import {getIODescription} from 'shared/ReactIODescription';
128
129 +import {
130 + getPublicInstance,
131 + getNativeTag,
132 + getCurrentTime,
133 +} from 'react-devtools-shared/src/backend/DevToolsNativeHost';
134 +import {
135 + isError,
136 + rootSupportsProfiling,
137 + isErrorBoundary,
138 + getSecondaryEnvironmentName,
139 + areEqualRects,
140 +} from './shared/DevToolsFiberInspection';
141 +import {
142 + didFiberRender,
143 + getContextChanged,
144 + getChangedHooksIndices,
145 + getChangedKeys,
146 +} from './shared/DevToolsFiberChangeDetection';
147 +import {
148 + ioExistsInSuspenseAncestor,
149 + getAwaitInSuspendedByFromIO,
150 + getVirtualEndTime,
151 +} from './shared/DevToolsFiberSuspense';
152 +
153 import {
154 getStackByFiberInDevAndProd,
155 getOwnerStackByFiberInDev,
@@ -135,13 +157,6 @@ import {
157 supportsConsoleTasks,
158 } from './DevToolsFiberComponentStack';
159
138 -// $FlowFixMe[method-unbinding]
139 -const toString = Object.prototype.toString;
140 -
141 -function isError(object: mixed) {
142 - return toString.call(object) === '[object Error]';
143 -}
144 -
160 import {getStyleXData} from '../StyleX/utils';
161 import {createProfilingHooks} from '../profilingHooks';
162
@@ -160,6 +175,7 @@ import type {
175 ProfilingDataBackend,
176 ProfilingDataForRootBackend,
177 ReactRenderer,
178 + Rect,
179 RendererInterface,
180 SerializedElement,
181 SerializedAsyncInfo,
@@ -175,30 +191,21 @@ import type {
191 Plugins,
192 } from 'react-devtools-shared/src/frontend/types';
193 import type {ReactFunctionLocation} from 'shared/ReactTypes';
194 +import type {
195 + FiberInstance,
196 + FilteredFiberInstance,
197 + VirtualInstance,
198 + DevToolsInstance,
199 + SuspenseNode,
200 +} from './shared/DevToolsFiberTypes';
201 +import {
202 + FIBER_INSTANCE,
203 + VIRTUAL_INSTANCE,
204 + FILTERED_FIBER_INSTANCE,
205 +} from './shared/DevToolsFiberTypes';
206 import {getSourceLocationByFiber} from './DevToolsFiberComponentStack';
207 import {formatOwnerStack} from '../shared/DevToolsOwnerStack';
208
181 -// Kinds
182 -const FIBER_INSTANCE = 0;
183 -const VIRTUAL_INSTANCE = 1;
184 -const FILTERED_FIBER_INSTANCE = 2;
185 -
186 -// This type represents a stateful instance of a Client Component i.e. a Fiber pair.
187 -// These instances also let us track stateful DevTools meta data like id and warnings.
188 -type FiberInstance = {
189 - kind: 0,
190 - id: number,
191 - parent: null | DevToolsInstance,
192 - firstChild: null | DevToolsInstance,
193 - nextSibling: null | DevToolsInstance,
194 - source: null | string | Error | ReactFunctionLocation, // source location of this component function, or owned child stack
195 - logCount: number, // total number of errors/warnings last seen
196 - treeBaseDuration: number, // the profiled time of the last render of this subtree
197 - suspendedBy: null | Array<ReactAsyncInfo>, // things that suspended in the children position of this component
198 - suspenseNode: null | SuspenseNode,
199 - data: Fiber, // one of a Fiber pair
200 -};
201 -
209 function createFiberInstance(fiber: Fiber): FiberInstance {
210 return {
211 kind: FIBER_INSTANCE,
@@ -215,22 +222,6 @@ function createFiberInstance(fiber: Fiber): FiberInstance {
222 };
223 }
224
218 -type FilteredFiberInstance = {
219 - kind: 2,
220 - // We exclude id from the type to get errors if we try to access it.
221 - // However it is still in the object to preserve hidden class.
222 - // id: number,
223 - parent: null | DevToolsInstance,
224 - firstChild: null | DevToolsInstance,
225 - nextSibling: null | DevToolsInstance,
226 - source: null | string | Error | ReactFunctionLocation, // always null here.
227 - logCount: number, // total number of errors/warnings last seen
228 - treeBaseDuration: number, // the profiled time of the last render of this subtree
229 - suspendedBy: null | Array<ReactAsyncInfo>, // only used at the root
230 - suspenseNode: null | SuspenseNode,
231 - data: Fiber, // one of a Fiber pair
232 -};
233 -
225 // This is used to represent a filtered Fiber but still lets us find its host instance.
226 function createFilteredFiberInstance(fiber: Fiber): FilteredFiberInstance {
227 return ({
@@ -248,27 +239,6 @@ function createFilteredFiberInstance(fiber: Fiber): FilteredFiberInstance {
239 }: any);
240 }
241
251 -// This type represents a stateful instance of a Server Component or a Component
252 -// that gets optimized away - e.g. call-through without creating a Fiber.
253 -// It's basically a virtual Fiber. This is not a semantic concept in React.
254 -// It only exists as a virtual concept to let the same Element in the DevTools
255 -// persist. To be selectable separately from all ReactComponentInfo and overtime.
256 -type VirtualInstance = {
257 - kind: 1,
258 - id: number,
259 - parent: null | DevToolsInstance,
260 - firstChild: null | DevToolsInstance,
261 - nextSibling: null | DevToolsInstance,
262 - source: null | string | Error | ReactFunctionLocation, // source location of this server component, or owned child stack
263 - logCount: number, // total number of errors/warnings last seen
264 - treeBaseDuration: number, // the profiled time of the last render of this subtree
265 - suspendedBy: null | Array<ReactAsyncInfo>, // things that blocked the server component's child from rendering
266 - suspenseNode: null,
267 - // The latest info for this instance. This can be updated over time and the
268 - // same info can appear in more than once ServerComponentInstance.
269 - data: ReactComponentInfo,
270 -};
271 -
242 function createVirtualInstance(
243 debugEntry: ReactComponentInfo,
244 ): VirtualInstance {
@@ -287,30 +257,6 @@ function createVirtualInstance(
257 };
258 }
259
290 -type DevToolsInstance = FiberInstance | VirtualInstance | FilteredFiberInstance;
291 -
292 -// A Generic Rect super type which can include DOMRect and other objects with similar shape like in React Native.
293 -type Rect = {x: number, y: number, width: number, height: number, ...};
294 -
295 -type SuspenseNode = {
296 - // The Instance can be a Suspense boundary, a SuspenseList Row, or HostRoot.
297 - // It can also be disconnected from the main tree if it's a Filtered Instance.
298 - instance: FiberInstance | FilteredFiberInstance,
299 - parent: null | SuspenseNode,
300 - firstChild: null | SuspenseNode,
301 - nextSibling: null | SuspenseNode,
302 - rects: null | Array<Rect>, // The bounding rects of content children.
303 - suspendedBy: Map<ReactIOInfo, Set<DevToolsInstance>>, // Tracks which data we're suspended by and the children that suspend it.
304 - environments: Map<string, number>, // Tracks the Flight environment names that suspended this. I.e. if the server blocked this.
305 - endTime: number, // Track a short cut to the maximum end time value within the suspendedBy set.
306 - // Track whether any of the items in suspendedBy are unique this this Suspense boundaries or if they're all
307 - // also in the parent sets. This determine whether this could contribute in the loading sequence.
308 - hasUniqueSuspenders: boolean,
309 - // Track whether anything suspended in this boundary that we can't track either because it was using throw
310 - // a promise, an older version of React or because we're inspecting prod.
311 - hasUnknownSuspenders: boolean,
312 -};
313 -
260 // Update flags need to be propagated up until the caller that put the corresponding
261 // node on the stack.
262 // If you push a new node, you need to handle ShouldResetChildren when you pop it.
@@ -375,18 +321,6 @@ export function getDispatcherRef(renderer: {
321 return (injectedRef: any);
322 }
323
378 -function getFiberFlags(fiber: Fiber): number {
379 - // The name of this field changed from "effectTag" to "flags"
380 - return fiber.flags !== undefined ? fiber.flags : (fiber: any).effectTag;
381 -}
382 -
383 -// Some environments (e.g. React Native / Hermes) don't support the performance API yet.
384 -const getCurrentTime =
385 - // $FlowFixMe[method-unbinding]
386 - typeof performance === 'object' && typeof performance.now === 'function'
387 - ? () => performance.now()
388 - : () => Date.now();
389 -
324 export function getInternalReactConstants(version: string): {
325 getDisplayNameForFiber: getDisplayNameForFiberType,
326 getTypeSymbol: getTypeSymbolType,
@@ -883,53 +817,6 @@ const hostResourceToDevToolsInstanceMap: Map<
817 Set<DevToolsInstance>,
818 > = new Map();
819
886 -// Ideally, this should be injected from Reconciler config
887 -function getPublicInstance(instance: HostInstance): HostInstance {
888 - // Typically the PublicInstance and HostInstance is the same thing but not in Fabric.
889 - // So we need to detect this and use that as the public instance.
890 -
891 - // React Native. Modern. Fabric.
892 - if (typeof instance === 'object' && instance !== null) {
893 - if (typeof instance.canonical === 'object' && instance.canonical !== null) {
894 - if (
895 - typeof instance.canonical.publicInstance === 'object' &&
896 - instance.canonical.publicInstance !== null
897 - ) {
898 - return instance.canonical.publicInstance;
899 - }
900 - }
901 -
902 - // React Native. Legacy. Paper.
903 - if (typeof instance._nativeTag === 'number') {
904 - return instance._nativeTag;
905 - }
906 - }
907 -
908 - // React Web. Usually a DOM element.
909 - return instance;
910 -}
911 -
912 -function getNativeTag(instance: HostInstance): number | null {
913 - if (typeof instance !== 'object' || instance === null) {
914 - return null;
915 - }
916 -
917 - // Modern. Fabric.
918 - if (
919 - instance.canonical != null &&
920 - typeof instance.canonical.nativeTag === 'number'
921 - ) {
922 - return instance.canonical.nativeTag;
923 - }
924 -
925 - // Legacy. Paper.
926 - if (typeof instance._nativeTag === 'number') {
927 - return instance._nativeTag;
928 - }
929 -
930 - return null;
931 -}
932 -
820 function aquireHostInstance(
821 nearestInstance: DevToolsInstance,
822 hostInstance: HostInstance,
@@ -1029,7 +916,6 @@ export function attach(
916 const {
917 ActivityComponent,
918 ClassComponent,
1032 - ContextConsumer,
919 DehydratedSuspenseComponent,
920 ForwardRef,
921 Fragment,
@@ -1992,134 +1878,6 @@ export function attach(
1878 }
1879 }
1880
1995 - function getContextChanged(prevFiber: Fiber, nextFiber: Fiber): boolean {
1996 - let prevContext =
1997 - prevFiber.dependencies && prevFiber.dependencies.firstContext;
1998 - let nextContext =
1999 - nextFiber.dependencies && nextFiber.dependencies.firstContext;
2000 -
2001 - while (prevContext && nextContext) {
2002 - // Note this only works for versions of React that support this key (e.v. 18+)
2003 - // For older versions, there's no good way to read the current context value after render has completed.
2004 - // This is because React maintains a stack of context values during render,
2005 - // but by the time DevTools is called, render has finished and the stack is empty.
2006 - if (prevContext.context !== nextContext.context) {
2007 - // If the order of context has changed, then the later context values might have
2008 - // changed too but the main reason it rerendered was earlier. Either an earlier
2009 - // context changed value but then we would have exited already. If we end up here
2010 - // it's because a state or props change caused the order of contexts used to change.
2011 - // So the main cause is not the contexts themselves.
2012 - return false;
2013 - }
2014 - if (!is(prevContext.memoizedValue, nextContext.memoizedValue)) {
2015 - return true;
2016 - }
2017 -
2018 - prevContext = prevContext.next;
2019 - nextContext = nextContext.next;
2020 - }
2021 - return false;
2022 - }
2023 -
2024 - function didStatefulHookChange(prev: HooksNode, next: HooksNode): boolean {
2025 - // Detect the shape of useState() / useReducer() / useTransition() / useSyncExternalStore() / useActionState()
2026 - const isStatefulHook =
2027 - prev.isStateEditable === true ||
2028 - prev.name === 'SyncExternalStore' ||
2029 - prev.name === 'Transition' ||
2030 - prev.name === 'ActionState' ||
2031 - prev.name === 'FormState';
2032 -
2033 - // Compare the values to see if they changed
2034 - if (isStatefulHook) {
2035 - return prev.value !== next.value;
2036 - }
2037 -
2038 - return false;
2039 - }
2040 -
2041 - function getChangedHooksIndices(
2042 - prevHooks: HooksTree | null,
2043 - nextHooks: HooksTree | null,
2044 - ): null | Array<number> {
2045 - if (prevHooks == null || nextHooks == null) {
2046 - return null;
2047 - }
2048 -
2049 - const indices: Array<number> = [];
2050 - let index = 0;
2051 -
2052 - function traverse(prevTree: HooksTree, nextTree: HooksTree): void {
2053 - for (let i = 0; i < prevTree.length; i++) {
2054 - const prevHook = prevTree[i];
2055 - const nextHook = nextTree[i];
2056 -
2057 - if (prevHook.subHooks.length > 0 && nextHook.subHooks.length > 0) {
2058 - traverse(prevHook.subHooks, nextHook.subHooks);
2059 - continue;
2060 - }
2061 -
2062 - if (didStatefulHookChange(prevHook, nextHook)) {
2063 - indices.push(index);
2064 - }
2065 -
2066 - index++;
2067 - }
2068 - }
2069 -
2070 - traverse(prevHooks, nextHooks);
2071 - return indices;
2072 - }
2073 -
2074 - function getChangedKeys(prev: any, next: any): null | Array<string> {
2075 - if (prev == null || next == null) {
2076 - return null;
2077 - }
2078 -
2079 - const keys = new Set([...Object.keys(prev), ...Object.keys(next)]);
2080 - const changedKeys = [];
2081 - // eslint-disable-next-line no-for-of-loops/no-for-of-loops
2082 - for (const key of keys) {
2083 - if (prev[key] !== next[key]) {
2084 - changedKeys.push(key);
2085 - }
2086 - }
2087 -
2088 - return changedKeys;
2089 - }
2090 -
2091 - /**
2092 - * Returns true iff nextFiber actually performed any work and produced an update.
2093 - * For generic components, like Function or Class components, prevFiber is not considered.
2094 - */
2095 - function didFiberRender(prevFiber: Fiber, nextFiber: Fiber): boolean {
2096 - switch (nextFiber.tag) {
2097 - case ClassComponent:
2098 - case FunctionComponent:
2099 - case ContextConsumer:
2100 - case MemoComponent:
2101 - case SimpleMemoComponent:
2102 - case ForwardRef:
2103 - // For types that execute user code, we check PerformedWork effect.
2104 - // We don't reflect bailouts (either referential or sCU) in DevTools.
2105 - // TODO: This flag is a leaked implementation detail. Once we start
2106 - // releasing DevTools in lockstep with React, we should import a
2107 - // function from the reconciler instead.
2108 - const PerformedWork = 0b000000000000000000000000001;
2109 - return (getFiberFlags(nextFiber) & PerformedWork) === PerformedWork;
2110 - // Note: ContextConsumer only gets PerformedWork effect in 16.3.3+
2111 - // so it won't get highlighted with React 16.3.0 to 16.3.2.
2112 - default:
2113 - // For host components and other types, we compare inputs
2114 - // to determine whether something is an update.
2115 - return (
2116 - prevFiber.memoizedProps !== nextFiber.memoizedProps ||
2117 - prevFiber.memoizedState !== nextFiber.memoizedState ||
2118 - prevFiber.ref !== nextFiber.ref
2119 - );
2120 - }
2121 - }
2122 -
1881 type OperationsArray = Array<number>;
1882
1883 type StringTableEntry = {
@@ -2943,20 +2701,6 @@ export function attach(
2701 // the current parent here as well.
2702 let reconcilingParentSuspenseNode: null | SuspenseNode = null;
2703
2946 - function ioExistsInSuspenseAncestor(
2947 - suspenseNode: SuspenseNode,
2948 - ioInfo: ReactIOInfo,
2949 - ): boolean {
2950 - let ancestor = suspenseNode.parent;
2951 - while (ancestor !== null) {
2952 - if (ancestor.suspendedBy.has(ioInfo)) {
2953 - return true;
2954 - }
2955 - ancestor = ancestor.parent;
2956 - }
2957 - return false;
2958 - }
2959 -
2704 function insertSuspendedBy(asyncInfo: ReactAsyncInfo): void {
2705 if (reconcilingParent === null || reconcilingParentSuspenseNode === null) {
2706 throw new Error(
@@ -3055,19 +2799,6 @@ export function attach(
2799 }
2800 }
2801
3058 - function getAwaitInSuspendedByFromIO(
3059 - suspensedBy: Array<ReactAsyncInfo>,
3060 - ioInfo: ReactIOInfo,
3061 - ): null | ReactAsyncInfo {
3062 - for (let i = 0; i < suspensedBy.length; i++) {
3063 - const asyncInfo = suspensedBy[i];
3064 - if (asyncInfo.awaited === ioInfo) {
3065 - return asyncInfo;
3066 - }
3067 - }
3068 - return null;
3069 - }
3070 -
2802 function unblockSuspendedBy(
2803 parentSuspenseNode: SuspenseNode,
2804 ioInfo: ReactIOInfo,
@@ -3101,15 +2832,6 @@ export function attach(
2832 }
2833 }
2834
3104 - function getVirtualEndTime(ioInfo: ReactIOInfo): number {
3105 - if (ioInfo.env != null) {
3106 - // Sort client side content first so that scripts and streams don't
3107 - // cover up the effect of server time.
3108 - return ioInfo.end + 1000000;
3109 - }
3110 - return ioInfo.end;
3111 - }
3112 -
2835 function computeEndTime(suspenseNode: SuspenseNode) {
2836 let maxEndTime = 0;
2837 suspenseNode.suspendedBy.forEach((set, ioInfo) => {
@@ -3432,34 +3154,6 @@ export function attach(
3154 return false;
3155 }
3156
3435 - function areEqualRects(
3436 - a: null | Array<Rect>,
3437 - b: null | Array<Rect>,
3438 - ): boolean {
3439 - if (a === null) {
3440 - return b === null;
3441 - }
3442 - if (b === null) {
3443 - return false;
3444 - }
3445 - if (a.length !== b.length) {
3446 - return false;
3447 - }
3448 - for (let i = 0; i < a.length; i++) {
3449 - const aRect = a[i];
3450 - const bRect = b[i];
3451 - if (
3452 - aRect.x !== bRect.x ||
3453 - aRect.y !== bRect.y ||
3454 - aRect.width !== bRect.width ||
3455 - aRect.height !== bRect.height
3456 - ) {
3457 - return false;
3458 - }
3459 - }
3460 - return true;
3461 - }
3462 -
3157 function measureUnchangedSuspenseNodesRecursively(
3158 suspenseNode: SuspenseNode,
3159 ): void {
@@ -3632,25 +3326,6 @@ export function attach(
3326 pendingRealUnmountedIDs.push(id);
3327 }
3328
3635 - function getSecondaryEnvironmentName(
3636 - debugInfo: ?ReactDebugInfo,
3637 - index: number,
3638 - ): null | string {
3639 - if (debugInfo != null) {
3640 - const componentInfo: ReactComponentInfo = (debugInfo[index]: any);
3641 - for (let i = index + 1; i < debugInfo.length; i++) {
3642 - const debugEntry = debugInfo[i];
3643 - if (typeof debugEntry.env === 'string') {
3644 - // If the next environment is different then this component was the boundary
3645 - // and it changed before entering the next component. So we assign this
3646 - // component a secondary environment.
3647 - return componentInfo.env !== debugEntry.env ? debugEntry.env : null;
3648 - }
3649 - }
3650 - }
3651 - return null;
3652 - }
3653 -
3329 function trackDebugInfoFromLazyType(fiber: Fiber): void {
3330 // The debugInfo from a Lazy isn't propagated onto _debugInfo of the parent Fiber the way
3331 // it is when used in child position. So we need to pick it up explicitly.
@@ -4532,7 +4207,8 @@ export function attach(
4207
4208 if (
4209 prevFiber == null ||
4535 - (prevFiber !== fiber && didFiberRender(prevFiber, fiber))
4210 + (prevFiber !== fiber &&
4211 + didFiberRender(ReactTypeOfWork, prevFiber, fiber))
4212 ) {
4213 if (actualDuration != null) {
4214 // The actual duration reported by React includes time spent working on children.
@@ -5166,6 +4842,7 @@ export function attach(
4842 if (prevFiber !== nextFiber) {
4843 // Otherwise if this is a traced ancestor, flag for the nearest host descendant(s).
4844 traceNearestHostComponentUpdate = didFiberRender(
4845 + ReactTypeOfWork,
4846 prevFiber,
4847 nextFiber,
4848 );
@@ -5197,7 +4874,7 @@ export function attach(
4874 // Invalidating any Root invalidates the Screen too.
4875 (mostRecentlyInspectedElement.type === ElementTypeRoot &&
4876 nextFiber.tag === HostRoot)) &&
5200 - didFiberRender(prevFiber, nextFiber)
4877 + didFiberRender(ReactTypeOfWork, prevFiber, nextFiber)
4878 ) {
4879 // If this Fiber has updated, clear cached inspected data.
4880 // If it is inspected again, it may need to be re-run to obtain updated hooks values.
@@ -5720,22 +5397,6 @@ export function attach(
5397 isProfiling = false;
5398 }
5399
5723 - function rootSupportsProfiling(root: any) {
5724 - if (root.memoizedInteractions != null) {
5725 - // v16 builds include this field for the scheduler/tracing API.
5726 - return true;
5727 - } else if (
5728 - root.current != null &&
5729 - root.current.hasOwnProperty('treeBaseDuration')
5730 - ) {
5731 - // The scheduler/tracing API was removed in v17 though
5732 - // so we need to check a non-root Fiber.
5733 - return true;
5734 - } else {
5735 - return false;
5736 - }
5737 - }
5738 -
5400 function flushInitialOperations() {
5401 const localPendingOperationsQueue = pendingOperationsQueue;
5402
@@ -6804,23 +6465,6 @@ export function attach(
6465 return {instance, style};
6466 }
6467
6807 - function isErrorBoundary(fiber: Fiber): boolean {
6808 - const {tag, type} = fiber;
6809 -
6810 - switch (tag) {
6811 - case ClassComponent:
6812 - case IncompleteClassComponent:
6813 - const instance = fiber.stateNode;
6814 - return (
6815 - typeof type.getDerivedStateFromError === 'function' ||
6816 - (instance !== null &&
6817 - typeof instance.componentDidCatch === 'function')
6818 - );
6819 - default:
6820 - return false;
6821 - }
6822 - }
6823 -
6468 function inspectElementRaw(id: number): InspectedElement | null {
6469 const devtoolsInstance = idToDevToolsInstanceMap.get(id);
6470 if (devtoolsInstance === undefined) {
@@ -6995,7 +6639,7 @@ export function attach(
6639 current = current.return;
6640 if (temp.tag === SuspenseComponent) {
6641 hasSuspenseBoundary = true;
6998 - } else if (isErrorBoundary(temp)) {
6642 + } else if (isErrorBoundary(ReactTypeOfWork, temp)) {
6643 hasErrorBoundary = true;
6644 }
6645 }
@@ -7005,7 +6649,7 @@ export function attach(
6649 }
6650
6651 let isErrored = false;
7008 - if (isErrorBoundary(fiber)) {
6652 + if (isErrorBoundary(ReactTypeOfWork, fiber)) {
6653 // if the current inspected element is an error boundary,
6654 // either that we want to use it to toggle off error state
6655 // or that we allow to force error state on it if it's within another
@@ -7197,7 +6841,7 @@ export function attach(
6841 current = current.return;
6842 if (temp.tag === SuspenseComponent) {
6843 hasSuspenseBoundary = true;
7200 - } else if (isErrorBoundary(temp)) {
6844 + } else if (isErrorBoundary(ReactTypeOfWork, temp)) {
6845 hasErrorBoundary = true;
6846 }
6847 }
@@ -8314,7 +7958,7 @@ export function attach(
7958 return;
7959 }
7960 let fiber = nearestFiber;
8317 - while (!isErrorBoundary(fiber)) {
7961 + while (!isErrorBoundary(ReactTypeOfWork, fiber)) {
7962 if (fiber.return === null) {
7963 return;
7964 }
packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberChangeDetection.js new
+150
@@ -0,0 +1,150 @@
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 {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
12 +import type {WorkTagMap} from '../../types';
13 +
14 +import {getFiberFlags} from './DevToolsFiberInspection';
15 +import is from 'shared/objectIs';
16 +
17 +export function getContextChanged(prevFiber: Fiber, nextFiber: Fiber): boolean {
18 + let prevContext =
19 + prevFiber.dependencies && prevFiber.dependencies.firstContext;
20 + let nextContext =
21 + nextFiber.dependencies && nextFiber.dependencies.firstContext;
22 +
23 + while (prevContext && nextContext) {
24 + // Note this only works for versions of React that support this key (e.v. 18+)
25 + // For older versions, there's no good way to read the current context value after render has completed.
26 + // This is because React maintains a stack of context values during render,
27 + // but by the time DevTools is called, render has finished and the stack is empty.
28 + if (prevContext.context !== nextContext.context) {
29 + // If the order of context has changed, then the later context values might have
30 + // changed too but the main reason it rerendered was earlier. Either an earlier
31 + // context changed value but then we would have exited already. If we end up here
32 + // it's because a state or props change caused the order of contexts used to change.
33 + // So the main cause is not the contexts themselves.
34 + return false;
35 + }
36 + if (!is(prevContext.memoizedValue, nextContext.memoizedValue)) {
37 + return true;
38 + }
39 +
40 + prevContext = prevContext.next;
41 + nextContext = nextContext.next;
42 + }
43 + return false;
44 +}
45 +
46 +export function didStatefulHookChange(
47 + prev: HooksNode,
48 + next: HooksNode,
49 +): boolean {
50 + // Detect the shape of useState() / useReducer() / useTransition() / useSyncExternalStore() / useActionState()
51 + const isStatefulHook =
52 + prev.isStateEditable === true ||
53 + prev.name === 'SyncExternalStore' ||
54 + prev.name === 'Transition' ||
55 + prev.name === 'ActionState' ||
56 + prev.name === 'FormState';
57 +
58 + // Compare the values to see if they changed
59 + if (isStatefulHook) {
60 + return prev.value !== next.value;
61 + }
62 +
63 + return false;
64 +}
65 +
66 +export function getChangedHooksIndices(
67 + prevHooks: HooksTree | null,
68 + nextHooks: HooksTree | null,
69 +): null | Array<number> {
70 + if (prevHooks == null || nextHooks == null) {
71 + return null;
72 + }
73 +
74 + const indices: Array<number> = [];
75 + let index = 0;
76 +
77 + function traverse(prevTree: HooksTree, nextTree: HooksTree): void {
78 + for (let i = 0; i < prevTree.length; i++) {
79 + const prevHook = prevTree[i];
80 + const nextHook = nextTree[i];
81 +
82 + if (prevHook.subHooks.length > 0 && nextHook.subHooks.length > 0) {
83 + traverse(prevHook.subHooks, nextHook.subHooks);
84 + continue;
85 + }
86 +
87 + if (didStatefulHookChange(prevHook, nextHook)) {
88 + indices.push(index);
89 + }
90 +
91 + index++;
92 + }
93 + }
94 +
95 + traverse(prevHooks, nextHooks);
96 + return indices;
97 +}
98 +
99 +export function getChangedKeys(prev: any, next: any): null | Array<string> {
100 + if (prev == null || next == null) {
101 + return null;
102 + }
103 +
104 + const keys = new Set([...Object.keys(prev), ...Object.keys(next)]);
105 + const changedKeys = [];
106 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
107 + for (const key of keys) {
108 + if (prev[key] !== next[key]) {
109 + changedKeys.push(key);
110 + }
111 + }
112 +
113 + return changedKeys;
114 +}
115 +
116 +/**
117 + * Returns true iff nextFiber actually performed any work and produced an update.
118 + * For generic components, like Function or Class components, prevFiber is not considered.
119 + */
120 +export function didFiberRender(
121 + workTagMap: WorkTagMap,
122 + prevFiber: Fiber,
123 + nextFiber: Fiber,
124 +): boolean {
125 + switch (nextFiber.tag) {
126 + case workTagMap.ClassComponent:
127 + case workTagMap.FunctionComponent:
128 + case workTagMap.ContextConsumer:
129 + case workTagMap.MemoComponent:
130 + case workTagMap.SimpleMemoComponent:
131 + case workTagMap.ForwardRef:
132 + // For types that execute user code, we check PerformedWork effect.
133 + // We don't reflect bailouts (either referential or sCU) in DevTools.
134 + // TODO: This flag is a leaked implementation detail. Once we start
135 + // releasing DevTools in lockstep with React, we should import a
136 + // function from the reconciler instead.
137 + const PerformedWork = 0b000000000000000000000000001;
138 + return (getFiberFlags(nextFiber) & PerformedWork) === PerformedWork;
139 + // Note: ContextConsumer only gets PerformedWork effect in 16.3.3+
140 + // so it won't get highlighted with React 16.3.0 to 16.3.2.
141 + default:
142 + // For host components and other types, we compare inputs
143 + // to determine whether something is an update.
144 + return (
145 + prevFiber.memoizedProps !== nextFiber.memoizedProps ||
146 + prevFiber.memoizedState !== nextFiber.memoizedState ||
147 + prevFiber.ref !== nextFiber.ref
148 + );
149 + }
150 +}
packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberInspection.js new
+104
@@ -0,0 +1,104 @@
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 {ReactComponentInfo, ReactDebugInfo} from 'shared/ReactTypes';
11 +import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
12 +import type {WorkTagMap} from '../../types';
13 +import type {Rect} from '../../types';
14 +
15 +// $FlowFixMe[method-unbinding]
16 +const toString = Object.prototype.toString;
17 +
18 +export function isError(object: mixed): boolean {
19 + return toString.call(object) === '[object Error]';
20 +}
21 +
22 +export function getFiberFlags(fiber: Fiber): number {
23 + // The name of this field changed from "effectTag" to "flags"
24 + return fiber.flags !== undefined ? fiber.flags : (fiber: any).effectTag;
25 +}
26 +
27 +export function rootSupportsProfiling(root: any): boolean {
28 + if (root.memoizedInteractions != null) {
29 + // v16 builds include this field for the scheduler/tracing API.
30 + return true;
31 + } else if (
32 + root.current != null &&
33 + root.current.hasOwnProperty('treeBaseDuration')
34 + ) {
35 + // The scheduler/tracing API was removed in v17 though
36 + // so we need to check a non-root Fiber.
37 + return true;
38 + } else {
39 + return false;
40 + }
41 +}
42 +
43 +export function isErrorBoundary(workTagMap: WorkTagMap, fiber: Fiber): boolean {
44 + const {tag, type} = fiber;
45 +
46 + switch (tag) {
47 + case workTagMap.ClassComponent:
48 + case workTagMap.IncompleteClassComponent:
49 + const instance = fiber.stateNode;
50 + return (
51 + typeof type.getDerivedStateFromError === 'function' ||
52 + (instance !== null && typeof instance.componentDidCatch === 'function')
53 + );
54 + default:
55 + return false;
56 + }
57 +}
58 +
59 +export function getSecondaryEnvironmentName(
60 + debugInfo: ?ReactDebugInfo,
61 + index: number,
62 +): null | string {
63 + if (debugInfo != null) {
64 + const componentInfo: ReactComponentInfo = (debugInfo[index]: any);
65 + for (let i = index + 1; i < debugInfo.length; i++) {
66 + const debugEntry = debugInfo[i];
67 + if (typeof debugEntry.env === 'string') {
68 + // If the next environment is different then this component was the boundary
69 + // and it changed before entering the next component. So we assign this
70 + // component a secondary environment.
71 + return componentInfo.env !== debugEntry.env ? debugEntry.env : null;
72 + }
73 + }
74 + }
75 + return null;
76 +}
77 +
78 +export function areEqualRects(
79 + a: null | Array<Rect>,
80 + b: null | Array<Rect>,
81 +): boolean {
82 + if (a === null) {
83 + return b === null;
84 + }
85 + if (b === null) {
86 + return false;
87 + }
88 + if (a.length !== b.length) {
89 + return false;
90 + }
91 + for (let i = 0; i < a.length; i++) {
92 + const aRect = a[i];
93 + const bRect = b[i];
94 + if (
95 + aRect.x !== bRect.x ||
96 + aRect.y !== bRect.y ||
97 + aRect.width !== bRect.width ||
98 + aRect.height !== bRect.height
99 + ) {
100 + return false;
101 + }
102 + }
103 + return true;
104 +}
packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberSuspense.js new
+47
@@ -0,0 +1,47 @@
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 {ReactIOInfo, ReactAsyncInfo} from 'shared/ReactTypes';
11 +import type {SuspenseNode} from './DevToolsFiberTypes';
12 +
13 +export function ioExistsInSuspenseAncestor(
14 + suspenseNode: SuspenseNode,
15 + ioInfo: ReactIOInfo,
16 +): boolean {
17 + let ancestor = suspenseNode.parent;
18 + while (ancestor !== null) {
19 + if (ancestor.suspendedBy.has(ioInfo)) {
20 + return true;
21 + }
22 + ancestor = ancestor.parent;
23 + }
24 + return false;
25 +}
26 +
27 +export function getAwaitInSuspendedByFromIO(
28 + suspensedBy: Array<ReactAsyncInfo>,
29 + ioInfo: ReactIOInfo,
30 +): null | ReactAsyncInfo {
31 + for (let i = 0; i < suspensedBy.length; i++) {
32 + const asyncInfo = suspensedBy[i];
33 + if (asyncInfo.awaited === ioInfo) {
34 + return asyncInfo;
35 + }
36 + }
37 + return null;
38 +}
39 +
40 +export function getVirtualEndTime(ioInfo: ReactIOInfo): number {
41 + if (ioInfo.env != null) {
42 + // Sort client side content first so that scripts and streams don't
43 + // cover up the effect of server time.
44 + return ioInfo.end + 1000000;
45 + }
46 + return ioInfo.end;
47 +}
packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberTypes.js new
+99
@@ -0,0 +1,99 @@
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 {
12 + ReactComponentInfo,
13 + ReactAsyncInfo,
14 + ReactIOInfo,
15 + ReactFunctionLocation,
16 +} from 'shared/ReactTypes';
17 +import type {Rect} from '../../types';
18 +
19 +// Kinds
20 +export const FIBER_INSTANCE = 0;
21 +export const VIRTUAL_INSTANCE = 1;
22 +export const FILTERED_FIBER_INSTANCE = 2;
23 +
24 +// This type represents a stateful instance of a Client Component i.e. a Fiber pair.
25 +// These instances also let us track stateful DevTools meta data like id and warnings.
26 +export type FiberInstance = {
27 + kind: 0,
28 + id: number,
29 + parent: null | DevToolsInstance,
30 + firstChild: null | DevToolsInstance,
31 + nextSibling: null | DevToolsInstance,
32 + source: null | string | Error | ReactFunctionLocation, // source location of this component function, or owned child stack
33 + logCount: number, // total number of errors/warnings last seen
34 + treeBaseDuration: number, // the profiled time of the last render of this subtree
35 + suspendedBy: null | Array<ReactAsyncInfo>, // things that suspended in the children position of this component
36 + suspenseNode: null | SuspenseNode,
37 + data: Fiber, // one of a Fiber pair
38 +};
39 +
40 +export type FilteredFiberInstance = {
41 + kind: 2,
42 + // We exclude id from the type to get errors if we try to access it.
43 + // However it is still in the object to preserve hidden class.
44 + // id: number,
45 + parent: null | DevToolsInstance,
46 + firstChild: null | DevToolsInstance,
47 + nextSibling: null | DevToolsInstance,
48 + source: null | string | Error | ReactFunctionLocation, // always null here.
49 + logCount: number, // total number of errors/warnings last seen
50 + treeBaseDuration: number, // the profiled time of the last render of this subtree
51 + suspendedBy: null | Array<ReactAsyncInfo>, // only used at the root
52 + suspenseNode: null | SuspenseNode,
53 + data: Fiber, // one of a Fiber pair
54 +};
55 +
56 +// This type represents a stateful instance of a Server Component or a Component
57 +// that gets optimized away - e.g. call-through without creating a Fiber.
58 +// It's basically a virtual Fiber. This is not a semantic concept in React.
59 +// It only exists as a virtual concept to let the same Element in the DevTools
60 +// persist. To be selectable separately from all ReactComponentInfo and overtime.
61 +export type VirtualInstance = {
62 + kind: 1,
63 + id: number,
64 + parent: null | DevToolsInstance,
65 + firstChild: null | DevToolsInstance,
66 + nextSibling: null | DevToolsInstance,
67 + source: null | string | Error | ReactFunctionLocation, // source location of this server component, or owned child stack
68 + logCount: number, // total number of errors/warnings last seen
69 + treeBaseDuration: number, // the profiled time of the last render of this subtree
70 + suspendedBy: null | Array<ReactAsyncInfo>, // things that blocked the server component's child from rendering
71 + suspenseNode: null,
72 + // The latest info for this instance. This can be updated over time and the
73 + // same info can appear in more than once ServerComponentInstance.
74 + data: ReactComponentInfo,
75 +};
76 +
77 +export type DevToolsInstance =
78 + | FiberInstance
79 + | VirtualInstance
80 + | FilteredFiberInstance;
81 +
82 +export type SuspenseNode = {
83 + // The Instance can be a Suspense boundary, a SuspenseList Row, or HostRoot.
84 + // It can also be disconnected from the main tree if it's a Filtered Instance.
85 + instance: FiberInstance | FilteredFiberInstance,
86 + parent: null | SuspenseNode,
87 + firstChild: null | SuspenseNode,
88 + nextSibling: null | SuspenseNode,
89 + rects: null | Array<Rect>, // The bounding rects of content children.
90 + suspendedBy: Map<ReactIOInfo, Set<DevToolsInstance>>, // Tracks which data we're suspended by and the children that suspend it.
91 + environments: Map<string, number>, // Tracks the Flight environment names that suspended this. I.e. if the server blocked this.
92 + endTime: number, // Track a short cut to the maximum end time value within the suspendedBy set.
93 + // Track whether any of the items in suspendedBy are unique this this Suspense boundaries or if they're all
94 + // also in the parent sets. This determine whether this could contribute in the loading sequence.
95 + hasUniqueSuspenders: boolean,
96 + // Track whether anything suspended in this boundary that we can't track either because it was using throw
97 + // a promise, an older version of React or because we're inspecting prod.
98 + hasUnknownSuspenders: boolean,
99 +};
packages/react-devtools-shared/src/backend/types.js
+1 -1
@@ -101,7 +101,7 @@ export type FindHostInstancesForElementID = (
101 id: number,
102 ) => null | $ReadOnlyArray<HostInstance>;
103
104 -type Rect = {
104 +export type Rect = {
105 x: number,
106 y: number,
107 width: number,