[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.
Josh Story committed
Apr 20, 2023 at 15:08 UTC
cc93a8533264618b7a8e3d6fb56df917214c19d7
1 file changed
+61
-78
packages/shared/invokeGuardedCallbackImpl.js
+61
-78
@@ -7,50 +7,8 @@
7
* @flow
8
*/
9
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);
11
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
12
if (
13
typeof window !== 'undefined' &&
14
typeof window.dispatchEvent === 'function' &&
@@ -58,29 +16,37 @@ if (__DEV__) {
16
// $FlowFixMe[method-unbinding]
17
typeof document.createEvent === 'function'
18
) {
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
+}
22
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) {
50
const evt = document.createEvent('Event');
51
52
let didCall = false;
@@ -104,7 +70,7 @@ if (__DEV__) {
70
'event',
71
);
72
107
- function restoreAfterDispatch() {
73
+ const restoreAfterDispatch = () => {
74
// We immediately remove the callback from event listeners so that
75
// nested `invokeGuardedCallback` calls do not clash. Otherwise, a
76
// nested call would trigger the fake event handlers of any call higher
@@ -121,20 +87,20 @@ if (__DEV__) {
87
) {
88
window.event = windowEvent;
89
}
124
- }
90
+ };
91
92
// Create an event handler for our fake event. We will synchronously
93
// dispatch our fake event using `dispatchEvent`. Inside the handler, we
94
// call the user-provided callback.
95
// $FlowFixMe[method-unbinding]
96
const funcArgs = Array.prototype.slice.call(arguments, 3);
131
- function callCallback() {
97
+ const callCallback = () => {
98
didCall = true;
99
restoreAfterDispatch();
100
// $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
101
func.apply(context, funcArgs);
102
didError = false;
137
- }
103
+ };
104
105
// Create a global error event handler. We use this to capture the value
106
// that was thrown. It's possible that this error handler will fire more
@@ -152,8 +118,7 @@ if (__DEV__) {
118
let didSetError = false;
119
let isCrossOriginError = false;
120
155
- // $FlowFixMe[missing-local-annot]
156
- function handleWindowError(event) {
121
+ const handleWindowError = (event: ErrorEvent) => {
122
error = event.error;
123
didSetError = true;
124
if (error === null && event.colno === 0 && event.lineno === 0) {
@@ -171,7 +136,7 @@ if (__DEV__) {
136
}
137
}
138
}
174
- }
139
+ };
140
141
// Create a fake event type.
142
const evtType = `react-${name ? name : 'invokeguardedcallback'}`;
@@ -184,7 +149,6 @@ if (__DEV__) {
149
// errors, it will trigger our global error handler.
150
evt.initEvent(evtType, false, false);
151
fakeNode.dispatchEvent(evt);
187
-
152
if (windowEventDescriptor) {
153
Object.defineProperty(window, 'event', windowEventDescriptor);
154
}
@@ -217,16 +181,35 @@ if (__DEV__) {
181
// Remove our event listeners
182
window.removeEventListener('error', handleWindowError);
183
220
- if (!didCall) {
184
+ if (didCall) {
185
+ return;
186
+ } else {
187
// Something went really wrong, and our event was not dispatched.
188
// https://github.com/facebook/react/issues/16734
189
// https://github.com/facebook/react/issues/16585
190
// Fall back to the production implementation.
191
restoreAfterDispatch();
226
- return invokeGuardedCallbackProd.apply(this, arguments);
192
+ // we fall through and call the prod version instead
193
}
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
+ }
214
}
215
}
231
-
232
-export default invokeGuardedCallbackImpl;