Skip to content

Commit e382475

Browse files
gnoffkassens
authored andcommitted
[Fiber] InvokeGuardedCallback without metaprogramming (#26569)
InvokeGuardedCallback is now implemented with the browser fork done at error-time rather than module-load-time. Originally it also tried to freeze the window/document references to avoid mismatches in prototype chains when testing React in different documents however we have since updated our tests to not do this and it was a test only feature so I removed it.
1 parent 1fb0306 commit e382475

File tree

1 file changed

+61
-78
lines changed

1 file changed

+61
-78
lines changed

packages/shared/invokeGuardedCallbackImpl.js

Lines changed: 61 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -7,80 +7,46 @@
77
* @flow
88
*/
99

10-
// $FlowFixMe[missing-this-annot]
11-
function invokeGuardedCallbackProd<Args: Array<mixed>, Context>(
12-
name: string | null,
13-
func: (...Args) => mixed,
14-
context: Context,
15-
): void {
16-
// $FlowFixMe[method-unbinding]
17-
const funcArgs = Array.prototype.slice.call(arguments, 3);
18-
try {
19-
// $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
20-
func.apply(context, funcArgs);
21-
} catch (error) {
22-
this.onError(error);
23-
}
24-
}
25-
26-
let invokeGuardedCallbackImpl: <Args: Array<mixed>, Context>(
27-
name: string | null,
28-
func: (...Args) => mixed,
29-
context: Context,
30-
) => void = invokeGuardedCallbackProd;
31-
10+
let fakeNode: Element = (null: any);
3211
if (__DEV__) {
33-
// In DEV mode, we swap out invokeGuardedCallback for a special version
34-
// that plays more nicely with the browser's DevTools. The idea is to preserve
35-
// "Pause on exceptions" behavior. Because React wraps all user-provided
36-
// functions in invokeGuardedCallback, and the production version of
37-
// invokeGuardedCallback uses a try-catch, all user exceptions are treated
38-
// like caught exceptions, and the DevTools won't pause unless the developer
39-
// takes the extra step of enabling pause on caught exceptions. This is
40-
// unintuitive, though, because even though React has caught the error, from
41-
// the developer's perspective, the error is uncaught.
42-
//
43-
// To preserve the expected "Pause on exceptions" behavior, we don't use a
44-
// try-catch in DEV. Instead, we synchronously dispatch a fake event to a fake
45-
// DOM node, and call the user-provided callback from inside an event handler
46-
// for that fake event. If the callback throws, the error is "captured" using
47-
// a global event handler. But because the error happens in a different
48-
// event loop context, it does not interrupt the normal program flow.
49-
// Effectively, this gives us try-catch behavior without actually using
50-
// try-catch. Neat!
51-
52-
// Check that the browser supports the APIs we need to implement our special
53-
// DEV version of invokeGuardedCallback
5412
if (
5513
typeof window !== 'undefined' &&
5614
typeof window.dispatchEvent === 'function' &&
5715
typeof document !== 'undefined' &&
5816
// $FlowFixMe[method-unbinding]
5917
typeof document.createEvent === 'function'
6018
) {
61-
const fakeNode = document.createElement('react');
62-
63-
invokeGuardedCallbackImpl = function invokeGuardedCallbackDev<
64-
Args: Array<mixed>,
65-
Context,
66-
// $FlowFixMe[missing-this-annot]
67-
>(name: string | null, func: (...Args) => mixed, context: Context): void {
68-
// If document doesn't exist we know for sure we will crash in this method
69-
// when we call document.createEvent(). However this can cause confusing
70-
// errors: https://github.com/facebook/create-react-app/issues/3482
71-
// So we preemptively throw with a better message instead.
72-
if (typeof document === 'undefined' || document === null) {
73-
throw new Error(
74-
'The `document` global was defined when React was initialized, but is not ' +
75-
'defined anymore. This can happen in a test environment if a component ' +
76-
'schedules an update from an asynchronous callback, but the test has already ' +
77-
'finished running. To solve this, you can either unmount the component at ' +
78-
'the end of your test (and ensure that any asynchronous operations get ' +
79-
'canceled in `componentWillUnmount`), or you can change the test itself ' +
80-
'to be asynchronous.',
81-
);
82-
}
19+
fakeNode = document.createElement('react');
20+
}
21+
}
8322

23+
export default function invokeGuardedCallbackImpl<Args: Array<mixed>, Context>(
24+
this: {onError: (error: mixed) => void},
25+
name: string | null,
26+
func: (...Args) => mixed,
27+
context: Context,
28+
): void {
29+
if (__DEV__) {
30+
// In DEV mode, we use a special version
31+
// that plays more nicely with the browser's DevTools. The idea is to preserve
32+
// "Pause on exceptions" behavior. Because React wraps all user-provided
33+
// functions in invokeGuardedCallback, and the production version of
34+
// invokeGuardedCallback uses a try-catch, all user exceptions are treated
35+
// like caught exceptions, and the DevTools won't pause unless the developer
36+
// takes the extra step of enabling pause on caught exceptions. This is
37+
// unintuitive, though, because even though React has caught the error, from
38+
// the developer's perspective, the error is uncaught.
39+
//
40+
// To preserve the expected "Pause on exceptions" behavior, we don't use a
41+
// try-catch in DEV. Instead, we synchronously dispatch a fake event to a fake
42+
// DOM node, and call the user-provided callback from inside an event handler
43+
// for that fake event. If the callback throws, the error is "captured" using
44+
// event loop context, it does not interrupt the normal program flow.
45+
// Effectively, this gives us try-catch behavior without actually using
46+
// try-catch. Neat!
47+
48+
// fakeNode signifies we are in an environment with a document and window object
49+
if (fakeNode) {
8450
const evt = document.createEvent('Event');
8551

8652
let didCall = false;
@@ -104,7 +70,7 @@ if (__DEV__) {
10470
'event',
10571
);
10672

107-
function restoreAfterDispatch() {
73+
const restoreAfterDispatch = () => {
10874
// We immediately remove the callback from event listeners so that
10975
// nested `invokeGuardedCallback` calls do not clash. Otherwise, a
11076
// nested call would trigger the fake event handlers of any call higher
@@ -121,20 +87,20 @@ if (__DEV__) {
12187
) {
12288
window.event = windowEvent;
12389
}
124-
}
90+
};
12591

12692
// Create an event handler for our fake event. We will synchronously
12793
// dispatch our fake event using `dispatchEvent`. Inside the handler, we
12894
// call the user-provided callback.
12995
// $FlowFixMe[method-unbinding]
13096
const funcArgs = Array.prototype.slice.call(arguments, 3);
131-
function callCallback() {
97+
const callCallback = () => {
13298
didCall = true;
13399
restoreAfterDispatch();
134100
// $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
135101
func.apply(context, funcArgs);
136102
didError = false;
137-
}
103+
};
138104

139105
// Create a global error event handler. We use this to capture the value
140106
// that was thrown. It's possible that this error handler will fire more
@@ -152,8 +118,7 @@ if (__DEV__) {
152118
let didSetError = false;
153119
let isCrossOriginError = false;
154120

155-
// $FlowFixMe[missing-local-annot]
156-
function handleWindowError(event) {
121+
const handleWindowError = (event: ErrorEvent) => {
157122
error = event.error;
158123
didSetError = true;
159124
if (error === null && event.colno === 0 && event.lineno === 0) {
@@ -171,7 +136,7 @@ if (__DEV__) {
171136
}
172137
}
173138
}
174-
}
139+
};
175140

176141
// Create a fake event type.
177142
const evtType = `react-${name ? name : 'invokeguardedcallback'}`;
@@ -184,7 +149,6 @@ if (__DEV__) {
184149
// errors, it will trigger our global error handler.
185150
evt.initEvent(evtType, false, false);
186151
fakeNode.dispatchEvent(evt);
187-
188152
if (windowEventDescriptor) {
189153
Object.defineProperty(window, 'event', windowEventDescriptor);
190154
}
@@ -217,16 +181,35 @@ if (__DEV__) {
217181
// Remove our event listeners
218182
window.removeEventListener('error', handleWindowError);
219183

220-
if (!didCall) {
184+
if (didCall) {
185+
return;
186+
} else {
221187
// Something went really wrong, and our event was not dispatched.
222188
// https://github.com/facebook/react/issues/16734
223189
// https://github.com/facebook/react/issues/16585
224190
// Fall back to the production implementation.
225191
restoreAfterDispatch();
226-
return invokeGuardedCallbackProd.apply(this, arguments);
192+
// we fall through and call the prod version instead
227193
}
228-
};
194+
}
195+
// We only get here if we are in an environment that either does not support the browser
196+
// variant or we had trouble getting the browser to emit the error.
197+
// $FlowFixMe[method-unbinding]
198+
const funcArgs = Array.prototype.slice.call(arguments, 3);
199+
try {
200+
// $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
201+
func.apply(context, funcArgs);
202+
} catch (error) {
203+
this.onError(error);
204+
}
205+
} else {
206+
// $FlowFixMe[method-unbinding]
207+
const funcArgs = Array.prototype.slice.call(arguments, 3);
208+
try {
209+
// $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
210+
func.apply(context, funcArgs);
211+
} catch (error) {
212+
this.onError(error);
213+
}
229214
}
230215
}
231-
232-
export default invokeGuardedCallbackImpl;

0 commit comments

Comments
 (0)