[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,