@samitouri / QOS-React / commits / da4abf0047

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