[Fiber] Call life-cycles with a react-stack-bottom-frame stack frame (#30429)
Stacked on #30427. Most hooks and such are called inside renders which already have these on the stack but life-cycles that call out on them are useful to cut off too. Typically we don't create JSX in here so they wouldn't be part of owner stacks anyway but they can be apart of plain stacks such as the ones prefixes to console logs or printed by error dialogs. This lets us cut off any React internals below. This should really be possible using just ignore listing too ideally. At this point we should maybe just build a Babel plugin that lets us annotate a function to need to have this name.
Sebastian Markbåge committed
Jul 23, 2024 at 18:49 UTC
da4abf0047cf6dc6d9bff505bd93815264c8c3b7
4 files changed
+263
-64
packages/react-devtools-shared/src/__tests__/console-test.js
+10
-11
@@ -254,12 +254,12 @@ describe('console', () => {
254
</Intermediate>
255
);
256
const Child = ({children}) => {
257
- React.useLayoutEffect(() => {
257
+ React.useLayoutEffect(function Child_useLayoutEffect() {
258
fakeConsole.error('active error');
259
fakeConsole.log('active log');
260
fakeConsole.warn('active warn');
261
});
262
- React.useEffect(() => {
262
+ React.useEffect(function Child_useEffect() {
263
fakeConsole.error('passive error');
264
fakeConsole.log('passive log');
265
fakeConsole.warn('passive warn');
@@ -279,15 +279,14 @@ describe('console', () => {
279
expect(mockWarn.mock.calls[0][0]).toBe('active warn');
280
expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
281
supportsOwnerStacks
282
- ? // TODO: It would be nice to have a Child stack frame here since it's just the effect function.
283
- '\n in Parent (at **)'
282
+ ? '\n in Child_useLayoutEffect (at **)\n in Parent (at **)'
283
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
284
);
285
expect(mockWarn.mock.calls[1]).toHaveLength(2);
286
expect(mockWarn.mock.calls[1][0]).toBe('passive warn');
287
expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual(
288
supportsOwnerStacks
290
- ? '\n in Parent (at **)'
289
+ ? '\n in Child_useEffect (at **)\n in Parent (at **)'
290
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
291
);
292
expect(mockError).toHaveBeenCalledTimes(2);
@@ -295,14 +294,14 @@ describe('console', () => {
294
expect(mockError.mock.calls[0][0]).toBe('active error');
295
expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
296
supportsOwnerStacks
298
- ? '\n in Parent (at **)'
297
+ ? '\n in Child_useLayoutEffect (at **)\n in Parent (at **)'
298
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
299
);
300
expect(mockError.mock.calls[1]).toHaveLength(2);
301
expect(mockError.mock.calls[1][0]).toBe('passive error');
302
expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe(
303
supportsOwnerStacks
305
- ? '\n in Parent (at **)'
304
+ ? '\n in Child_useEffect (at **)\n in Parent (at **)'
305
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
306
);
307
});
@@ -346,14 +345,14 @@ describe('console', () => {
345
expect(mockWarn.mock.calls[0][0]).toBe('didMount warn');
346
expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
347
supportsOwnerStacks
349
- ? '\n in Parent (at **)'
348
+ ? '\n in Child.componentDidMount (at **)\n in Parent (at **)'
349
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
350
);
351
expect(mockWarn.mock.calls[1]).toHaveLength(2);
352
expect(mockWarn.mock.calls[1][0]).toBe('didUpdate warn');
353
expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual(
354
supportsOwnerStacks
356
- ? '\n in Parent (at **)'
355
+ ? '\n in Child.componentDidUpdate (at **)\n in Parent (at **)'
356
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
357
);
358
expect(mockError).toHaveBeenCalledTimes(2);
@@ -361,14 +360,14 @@ describe('console', () => {
360
expect(mockError.mock.calls[0][0]).toBe('didMount error');
361
expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
362
supportsOwnerStacks
364
- ? '\n in Parent (at **)'
363
+ ? '\n in Child.componentDidMount (at **)\n in Parent (at **)'
364
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
365
);
366
expect(mockError.mock.calls[1]).toHaveLength(2);
367
expect(mockError.mock.calls[1][0]).toBe('didUpdate error');
368
expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe(
369
supportsOwnerStacks
371
- ? '\n in Parent (at **)'
370
+ ? '\n in Child.componentDidUpdate (at **)\n in Parent (at **)'
371
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
372
);
373
});
packages/react-reconciler/src/ReactFiberCallUserSpace.js
+150
@@ -7,9 +7,13 @@
7
* @flow
8
*/
9
10
+import type {Fiber} from './ReactInternalTypes';
11
import type {LazyComponent} from 'react/src/ReactLazy';
12
+import type {Effect} from './ReactFiberHooks';
13
+import type {CapturedValue} from './ReactCapturedValue';
14
15
import {isRendering, setIsRendering} from './ReactCurrentFiber';
16
+import {captureCommitPhaseError} from './ReactFiberWorkLoop';
17
18
// These indirections exists so we can exclude its stack frame in DEV (and anything below it).
19
// TODO: Consider marking the whole bundle instead of these boundaries.
@@ -42,6 +46,14 @@ export const callComponentInDEV: <Props, Arg, R>(
46
47
interface ClassInstance<R> {
48
render(): R;
49
+ componentDidMount(): void;
50
+ componentDidUpdate(
51
+ prevProps: Object,
52
+ prevState: Object,
53
+ snaphot: Object,
54
+ ): void;
55
+ componentDidCatch(error: mixed, errorInfo: {componentStack: string}): void;
56
+ componentWillUnmount(): void;
57
}
58
59
const callRender = {
@@ -63,6 +75,144 @@ export const callRenderInDEV: <R>(instance: ClassInstance<R>) => R => R =
75
(callRender['react-stack-bottom-frame'].bind(callRender): any)
76
: (null: any);
77
78
+const callComponentDidMount = {
79
+ 'react-stack-bottom-frame': function (
80
+ finishedWork: Fiber,
81
+ instance: ClassInstance<any>,
82
+ ): void {
83
+ try {
84
+ instance.componentDidMount();
85
+ } catch (error) {
86
+ captureCommitPhaseError(finishedWork, finishedWork.return, error);
87
+ }
88
+ },
89
+};
90
+
91
+export const callComponentDidMountInDEV: (
92
+ finishedWork: Fiber,
93
+ instance: ClassInstance<any>,
94
+) => void = __DEV__
95
+ ? // We use this technique to trick minifiers to preserve the function name.
96
+ (callComponentDidMount['react-stack-bottom-frame'].bind(
97
+ callComponentDidMount,
98
+ ): any)
99
+ : (null: any);
100
+
101
+const callComponentDidUpdate = {
102
+ 'react-stack-bottom-frame': function (
103
+ finishedWork: Fiber,
104
+ instance: ClassInstance<any>,
105
+ prevProps: Object,
106
+ prevState: Object,
107
+ snapshot: Object,
108
+ ): void {
109
+ try {
110
+ instance.componentDidUpdate(prevProps, prevState, snapshot);
111
+ } catch (error) {
112
+ captureCommitPhaseError(finishedWork, finishedWork.return, error);
113
+ }
114
+ },
115
+};
116
+
117
+export const callComponentDidUpdateInDEV: (
118
+ finishedWork: Fiber,
119
+ instance: ClassInstance<any>,
120
+ prevProps: Object,
121
+ prevState: Object,
122
+ snaphot: Object,
123
+) => void = __DEV__
124
+ ? // We use this technique to trick minifiers to preserve the function name.
125
+ (callComponentDidUpdate['react-stack-bottom-frame'].bind(
126
+ callComponentDidUpdate,
127
+ ): any)
128
+ : (null: any);
129
+
130
+const callComponentDidCatch = {
131
+ 'react-stack-bottom-frame': function (
132
+ instance: ClassInstance<any>,
133
+ errorInfo: CapturedValue<mixed>,
134
+ ): void {
135
+ const error = errorInfo.value;
136
+ const stack = errorInfo.stack;
137
+ instance.componentDidCatch(error, {
138
+ componentStack: stack !== null ? stack : '',
139
+ });
140
+ },
141
+};
142
+
143
+export const callComponentDidCatchInDEV: (
144
+ instance: ClassInstance<any>,
145
+ errorInfo: CapturedValue<mixed>,
146
+) => void = __DEV__
147
+ ? // We use this technique to trick minifiers to preserve the function name.
148
+ (callComponentDidCatch['react-stack-bottom-frame'].bind(
149
+ callComponentDidCatch,
150
+ ): any)
151
+ : (null: any);
152
+
153
+const callComponentWillUnmount = {
154
+ 'react-stack-bottom-frame': function (
155
+ current: Fiber,
156
+ nearestMountedAncestor: Fiber | null,
157
+ instance: ClassInstance<any>,
158
+ ): void {
159
+ try {
160
+ instance.componentWillUnmount();
161
+ } catch (error) {
162
+ captureCommitPhaseError(current, nearestMountedAncestor, error);
163
+ }
164
+ },
165
+};
166
+
167
+export const callComponentWillUnmountInDEV: (
168
+ current: Fiber,
169
+ nearestMountedAncestor: Fiber | null,
170
+ instance: ClassInstance<any>,
171
+) => void = __DEV__
172
+ ? // We use this technique to trick minifiers to preserve the function name.
173
+ (callComponentWillUnmount['react-stack-bottom-frame'].bind(
174
+ callComponentWillUnmount,
175
+ ): any)
176
+ : (null: any);
177
+
178
+const callCreate = {
179
+ 'react-stack-bottom-frame': function (effect: Effect): (() => void) | void {
180
+ const create = effect.create;
181
+ const inst = effect.inst;
182
+ const destroy = create();
183
+ inst.destroy = destroy;
184
+ return destroy;
185
+ },
186
+};
187
+
188
+export const callCreateInDEV: (effect: Effect) => (() => void) | void = __DEV__
189
+ ? // We use this technique to trick minifiers to preserve the function name.
190
+ (callCreate['react-stack-bottom-frame'].bind(callCreate): any)
191
+ : (null: any);
192
+
193
+const callDestroy = {
194
+ 'react-stack-bottom-frame': function (
195
+ current: Fiber,
196
+ nearestMountedAncestor: Fiber | null,
197
+ destroy: () => void,
198
+ ): void {
199
+ try {
200
+ destroy();
201
+ } catch (error) {
202
+ captureCommitPhaseError(current, nearestMountedAncestor, error);
203
+ }
204
+ },
205
+};
206
+
207
+export const callDestroyInDEV: (
208
+ current: Fiber,
209
+ nearestMountedAncestor: Fiber | null,
210
+ destroy: () => void,
211
+) => void = __DEV__
212
+ ? // We use this technique to trick minifiers to preserve the function name.
213
+ (callDestroy['react-stack-bottom-frame'].bind(callDestroy): any)
214
+ : (null: any);
215
+
216
const callLazyInit = {
217
'react-stack-bottom-frame': function (lazy: LazyComponent<any, any>): any {
218
const payload = lazy._payload;
packages/react-reconciler/src/ReactFiberCommitWork.js
+93
-48
@@ -213,6 +213,13 @@ import {
213
} from './ReactFiberTracingMarkerComponent';
214
import {scheduleUpdateOnFiber} from './ReactFiberWorkLoop';
215
import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates';
216
+import {
217
+ callComponentDidMountInDEV,
218
+ callComponentDidUpdateInDEV,
219
+ callComponentWillUnmountInDEV,
220
+ callCreateInDEV,
221
+ callDestroyInDEV,
222
+} from './ReactFiberCallUserSpace';
223
224
let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
225
if (__DEV__) {
@@ -244,7 +251,12 @@ function shouldProfile(current: Fiber): boolean {
251
);
252
}
253
247
-function callComponentWillUnmountWithTimer(current: Fiber, instance: any) {
254
+// Capture errors so they don't interrupt unmounting.
255
+function safelyCallComponentWillUnmount(
256
+ current: Fiber,
257
+ nearestMountedAncestor: Fiber | null,
258
+ instance: any,
259
+) {
260
instance.props = resolveClassComponentProps(
261
current.type,
262
current.memoizedProps,
@@ -252,27 +264,27 @@ function callComponentWillUnmountWithTimer(current: Fiber, instance: any) {
264
);
265
instance.state = current.memoizedState;
266
if (shouldProfile(current)) {
255
- try {
256
- startLayoutEffectTimer();
257
- instance.componentWillUnmount();
258
- } finally {
259
- recordLayoutEffectDuration(current);
267
+ startLayoutEffectTimer();
268
+ if (__DEV__) {
269
+ callComponentWillUnmountInDEV(current, nearestMountedAncestor, instance);
270
+ } else {
271
+ try {
272
+ instance.componentWillUnmount();
273
+ } catch (error) {
274
+ captureCommitPhaseError(current, nearestMountedAncestor, error);
275
+ }
276
}
277
+ recordLayoutEffectDuration(current);
278
} else {
262
- instance.componentWillUnmount();
263
- }
264
-}
265
-
266
-// Capture errors so they don't interrupt unmounting.
267
-function safelyCallComponentWillUnmount(
268
- current: Fiber,
269
- nearestMountedAncestor: Fiber | null,
270
- instance: any,
271
-) {
272
- try {
273
- callComponentWillUnmountWithTimer(current, instance);
274
- } catch (error) {
275
- captureCommitPhaseError(current, nearestMountedAncestor, error);
279
+ if (__DEV__) {
280
+ callComponentWillUnmountInDEV(current, nearestMountedAncestor, instance);
281
+ } else {
282
+ try {
283
+ instance.componentWillUnmount();
284
+ } catch (error) {
285
+ captureCommitPhaseError(current, nearestMountedAncestor, error);
286
+ }
287
+ }
288
}
289
}
290
@@ -339,10 +351,14 @@ function safelyCallDestroy(
351
nearestMountedAncestor: Fiber | null,
352
destroy: () => void,
353
) {
342
- try {
343
- destroy();
344
- } catch (error) {
345
- captureCommitPhaseError(current, nearestMountedAncestor, error);
354
+ if (__DEV__) {
355
+ callDestroyInDEV(current, nearestMountedAncestor, destroy);
356
+ } else {
357
+ try {
358
+ destroy();
359
+ } catch (error) {
360
+ captureCommitPhaseError(current, nearestMountedAncestor, error);
361
+ }
362
}
363
}
364
@@ -626,19 +642,20 @@ function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
642
}
643
644
// Mount
629
- const create = effect.create;
645
+ let destroy;
646
if (__DEV__) {
647
if ((flags & HookInsertion) !== NoHookEffect) {
648
setIsRunningInsertionEffect(true);
649
}
634
- }
635
- const inst = effect.inst;
636
- const destroy = create();
637
- inst.destroy = destroy;
638
- if (__DEV__) {
650
+ destroy = callCreateInDEV(effect);
651
if ((flags & HookInsertion) !== NoHookEffect) {
652
setIsRunningInsertionEffect(false);
653
}
654
+ } else {
655
+ const create = effect.create;
656
+ const inst = effect.inst;
657
+ destroy = create();
658
+ inst.destroy = destroy;
659
}
660
661
if (enableSchedulingProfiler) {
@@ -826,18 +843,26 @@ function commitClassLayoutLifecycles(
843
}
844
}
845
if (shouldProfile(finishedWork)) {
829
- try {
830
- startLayoutEffectTimer();
831
- instance.componentDidMount();
832
- } catch (error) {
833
- captureCommitPhaseError(finishedWork, finishedWork.return, error);
846
+ startLayoutEffectTimer();
847
+ if (__DEV__) {
848
+ callComponentDidMountInDEV(finishedWork, instance);
849
+ } else {
850
+ try {
851
+ instance.componentDidMount();
852
+ } catch (error) {
853
+ captureCommitPhaseError(finishedWork, finishedWork.return, error);
854
+ }
855
}
856
recordLayoutEffectDuration(finishedWork);
857
} else {
837
- try {
838
- instance.componentDidMount();
839
- } catch (error) {
840
- captureCommitPhaseError(finishedWork, finishedWork.return, error);
858
+ if (__DEV__) {
859
+ callComponentDidMountInDEV(finishedWork, instance);
860
+ } else {
861
+ try {
862
+ instance.componentDidMount();
863
+ } catch (error) {
864
+ captureCommitPhaseError(finishedWork, finishedWork.return, error);
865
+ }
866
}
867
}
868
} else {
@@ -879,26 +904,46 @@ function commitClassLayoutLifecycles(
904
}
905
}
906
if (shouldProfile(finishedWork)) {
882
- try {
883
- startLayoutEffectTimer();
884
- instance.componentDidUpdate(
907
+ startLayoutEffectTimer();
908
+ if (__DEV__) {
909
+ callComponentDidUpdateInDEV(
910
+ finishedWork,
911
+ instance,
912
prevProps,
913
prevState,
914
instance.__reactInternalSnapshotBeforeUpdate,
915
);
889
- } catch (error) {
890
- captureCommitPhaseError(finishedWork, finishedWork.return, error);
916
+ } else {
917
+ try {
918
+ instance.componentDidUpdate(
919
+ prevProps,
920
+ prevState,
921
+ instance.__reactInternalSnapshotBeforeUpdate,
922
+ );
923
+ } catch (error) {
924
+ captureCommitPhaseError(finishedWork, finishedWork.return, error);
925
+ }
926
}
927
recordLayoutEffectDuration(finishedWork);
928
} else {
894
- try {
895
- instance.componentDidUpdate(
929
+ if (__DEV__) {
930
+ callComponentDidUpdateInDEV(
931
+ finishedWork,
932
+ instance,
933
prevProps,
934
prevState,
935
instance.__reactInternalSnapshotBeforeUpdate,
936
);
900
- } catch (error) {
901
- captureCommitPhaseError(finishedWork, finishedWork.return, error);
937
+ } else {
938
+ try {
939
+ instance.componentDidUpdate(
940
+ prevProps,
941
+ prevState,
942
+ instance.__reactInternalSnapshotBeforeUpdate,
943
+ );
944
+ } catch (error) {
945
+ captureCommitPhaseError(finishedWork, finishedWork.return, error);
946
+ }
947
}
948
}
949
}
packages/react-reconciler/src/ReactFiberThrow.js
+10
-5
@@ -88,6 +88,7 @@ import {ConcurrentRoot} from './ReactRootTags';
88
import {noopSuspenseyCommitThenable} from './ReactFiberThenable';
89
import {REACT_POSTPONE_TYPE} from 'shared/ReactSymbols';
90
import {runWithFiberInDEV} from './ReactCurrentFiber';
91
+import {callComponentDidCatchInDEV} from './ReactFiberCallUserSpace';
92
93
function createRootErrorUpdate(
94
root: FiberRoot,
@@ -172,11 +173,15 @@ function initializeClassErrorUpdate(
173
// not defined.
174
markLegacyErrorBoundaryAsFailed(this);
175
}
175
- const error = errorInfo.value;
176
- const stack = errorInfo.stack;
177
- this.componentDidCatch(error, {
178
- componentStack: stack !== null ? stack : '',
179
- });
176
+ if (__DEV__) {
177
+ callComponentDidCatchInDEV(this, errorInfo);
178
+ } else {
179
+ const error = errorInfo.value;
180
+ const stack = errorInfo.stack;
181
+ this.componentDidCatch(error, {
182
+ componentStack: stack !== null ? stack : '',
183
+ });
184
+ }
185
if (__DEV__) {
186
if (typeof getDerivedStateFromError !== 'function') {
187
// If componentDidCatch is the only error boundary method defined,