Skip to content

Commit da4abf0

Browse files
authored
[Fiber] Call life-cycles with a react-stack-bottom-frame stack frame (facebook#30429)
Stacked on facebook#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.
1 parent e2cac67 commit da4abf0

File tree

4 files changed

+263
-64
lines changed

4 files changed

+263
-64
lines changed

packages/react-devtools-shared/src/__tests__/console-test.js

+10-11
Original file line numberDiff line numberDiff line change
@@ -254,12 +254,12 @@ describe('console', () => {
254254
</Intermediate>
255255
);
256256
const Child = ({children}) => {
257-
React.useLayoutEffect(() => {
257+
React.useLayoutEffect(function Child_useLayoutEffect() {
258258
fakeConsole.error('active error');
259259
fakeConsole.log('active log');
260260
fakeConsole.warn('active warn');
261261
});
262-
React.useEffect(() => {
262+
React.useEffect(function Child_useEffect() {
263263
fakeConsole.error('passive error');
264264
fakeConsole.log('passive log');
265265
fakeConsole.warn('passive warn');
@@ -279,30 +279,29 @@ describe('console', () => {
279279
expect(mockWarn.mock.calls[0][0]).toBe('active warn');
280280
expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
281281
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 **)'
284283
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
285284
);
286285
expect(mockWarn.mock.calls[1]).toHaveLength(2);
287286
expect(mockWarn.mock.calls[1][0]).toBe('passive warn');
288287
expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual(
289288
supportsOwnerStacks
290-
? '\n in Parent (at **)'
289+
? '\n in Child_useEffect (at **)\n in Parent (at **)'
291290
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
292291
);
293292
expect(mockError).toHaveBeenCalledTimes(2);
294293
expect(mockError.mock.calls[0]).toHaveLength(2);
295294
expect(mockError.mock.calls[0][0]).toBe('active error');
296295
expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
297296
supportsOwnerStacks
298-
? '\n in Parent (at **)'
297+
? '\n in Child_useLayoutEffect (at **)\n in Parent (at **)'
299298
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
300299
);
301300
expect(mockError.mock.calls[1]).toHaveLength(2);
302301
expect(mockError.mock.calls[1][0]).toBe('passive error');
303302
expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe(
304303
supportsOwnerStacks
305-
? '\n in Parent (at **)'
304+
? '\n in Child_useEffect (at **)\n in Parent (at **)'
306305
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
307306
);
308307
});
@@ -346,29 +345,29 @@ describe('console', () => {
346345
expect(mockWarn.mock.calls[0][0]).toBe('didMount warn');
347346
expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
348347
supportsOwnerStacks
349-
? '\n in Parent (at **)'
348+
? '\n in Child.componentDidMount (at **)\n in Parent (at **)'
350349
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
351350
);
352351
expect(mockWarn.mock.calls[1]).toHaveLength(2);
353352
expect(mockWarn.mock.calls[1][0]).toBe('didUpdate warn');
354353
expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual(
355354
supportsOwnerStacks
356-
? '\n in Parent (at **)'
355+
? '\n in Child.componentDidUpdate (at **)\n in Parent (at **)'
357356
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
358357
);
359358
expect(mockError).toHaveBeenCalledTimes(2);
360359
expect(mockError.mock.calls[0]).toHaveLength(2);
361360
expect(mockError.mock.calls[0][0]).toBe('didMount error');
362361
expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
363362
supportsOwnerStacks
364-
? '\n in Parent (at **)'
363+
? '\n in Child.componentDidMount (at **)\n in Parent (at **)'
365364
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
366365
);
367366
expect(mockError.mock.calls[1]).toHaveLength(2);
368367
expect(mockError.mock.calls[1][0]).toBe('didUpdate error');
369368
expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe(
370369
supportsOwnerStacks
371-
? '\n in Parent (at **)'
370+
? '\n in Child.componentDidUpdate (at **)\n in Parent (at **)'
372371
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
373372
);
374373
});

packages/react-reconciler/src/ReactFiberCallUserSpace.js

+150
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@
77
* @flow
88
*/
99

10+
import type {Fiber} from './ReactInternalTypes';
1011
import type {LazyComponent} from 'react/src/ReactLazy';
12+
import type {Effect} from './ReactFiberHooks';
13+
import type {CapturedValue} from './ReactCapturedValue';
1114

1215
import {isRendering, setIsRendering} from './ReactCurrentFiber';
16+
import {captureCommitPhaseError} from './ReactFiberWorkLoop';
1317

1418
// These indirections exists so we can exclude its stack frame in DEV (and anything below it).
1519
// TODO: Consider marking the whole bundle instead of these boundaries.
@@ -42,6 +46,14 @@ export const callComponentInDEV: <Props, Arg, R>(
4246

4347
interface ClassInstance<R> {
4448
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;
4557
}
4658

4759
const callRender = {
@@ -63,6 +75,144 @@ export const callRenderInDEV: <R>(instance: ClassInstance<R>) => R => R =
6375
(callRender['react-stack-bottom-frame'].bind(callRender): any)
6476
: (null: any);
6577

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+
66216
const callLazyInit = {
67217
'react-stack-bottom-frame': function (lazy: LazyComponent<any, any>): any {
68218
const payload = lazy._payload;

0 commit comments

Comments
 (0)