@samitouri / QOS-React-2 / commits / 8af1f87929

Rename ReactFiberScheduler -> ReactFiberWorkLoop and extract throwException from Unwind (#15725)

* Rename ReactFiberScheduler to ReactFiberWorkLoop The scheduling part is mostly extracted out to the scheduler package. What's remaining is mostly around the loop around each section of work. I name it something with Work in it because it's very related to the BeginWork, CompleteWork and UnwindWork sections. * Extract throwException from UnwindWork Our throwing works more like algebraic effects in that it's a separate phase where we find a handler and we later unwind.

Sebastian Markbåge committed May 23, 2019 at 14:24 UTC 8af1f879299e88f9e370c1f01d90efaeefcefd27
17 files changed +403 -383
packages/react-dom/src/test-utils/ReactTestUtils.js
+1 -1
@@ -4,7 +4,7 @@
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7 -import type {Thenable} from 'react-reconciler/src/ReactFiberScheduler';
7 +import type {Thenable} from 'react-reconciler/src/ReactFiberWorkLoop';
8
9 import React from 'react';
10 import ReactDOM from 'react-dom';
packages/react-dom/src/test-utils/ReactTestUtilsAct.js
+1 -1
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import type {Thenable} from 'react-reconciler/src/ReactFiberScheduler';
10 +import type {Thenable} from 'react-reconciler/src/ReactFiberWorkLoop';
11
12 import warningWithoutStack from 'shared/warningWithoutStack';
13 import ReactDOM from 'react-dom';
packages/react-noop-renderer/src/createReactNoop.js
+1 -1
@@ -14,7 +14,7 @@
14 * environment.
15 */
16
17 -import type {Thenable} from 'react-reconciler/src/ReactFiberScheduler';
17 +import type {Thenable} from 'react-reconciler/src/ReactFiberWorkLoop';
18 import type {Fiber} from 'react-reconciler/src/ReactFiber';
19 import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue';
20 import type {ReactNodeList} from 'shared/ReactTypes';
packages/react-reconciler/src/ReactFiberBeginWork.js
+1 -1
@@ -160,7 +160,7 @@ import {
160 createWorkInProgress,
161 isSimpleFunctionComponent,
162 } from './ReactFiber';
163 -import {requestCurrentTime, retryTimedOutBoundary} from './ReactFiberScheduler';
163 +import {requestCurrentTime, retryTimedOutBoundary} from './ReactFiberWorkLoop';
164
165 const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
166
packages/react-reconciler/src/ReactFiberClassComponent.js
+1 -1
@@ -53,7 +53,7 @@ import {
53 computeExpirationForFiber,
54 scheduleWork,
55 flushPassiveEffects,
56 -} from './ReactFiberScheduler';
56 +} from './ReactFiberWorkLoop';
57 import {revertPassiveEffectsChange} from 'shared/ReactFeatureFlags';
58 import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig';
59
packages/react-reconciler/src/ReactFiberCommitWork.js
+2 -2
@@ -21,7 +21,7 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
21 import type {CapturedValue, CapturedError} from './ReactCapturedValue';
22 import type {SuspenseState} from './ReactFiberSuspenseComponent';
23 import type {FunctionComponentUpdateQueue} from './ReactFiberHooks';
24 -import type {Thenable} from './ReactFiberScheduler';
24 +import type {Thenable} from './ReactFiberWorkLoop';
25
26 import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
27 import {
@@ -104,7 +104,7 @@ import {
104 captureCommitPhaseError,
105 requestCurrentTime,
106 resolveRetryThenable,
107 -} from './ReactFiberScheduler';
107 +} from './ReactFiberWorkLoop';
108 import {
109 NoEffect as NoHookEffect,
110 UnmountSnapshot,
packages/react-reconciler/src/ReactFiberCompleteWork.js
+1 -1
@@ -104,7 +104,7 @@ import {
104 markRenderEventTimeAndConfig,
105 renderDidSuspend,
106 renderDidSuspendDelayIfPossible,
107 -} from './ReactFiberScheduler';
107 +} from './ReactFiberWorkLoop';
108 import {getEventComponentHostChildrenCount} from './ReactFiberEvents';
109 import getComponentName from 'shared/getComponentName';
110 import warning from 'shared/warning';
packages/react-reconciler/src/ReactFiberDevToolsHook.js
+1 -1
@@ -8,7 +8,7 @@
8 */
9
10 import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
11 -import {requestCurrentTime} from './ReactFiberScheduler';
11 +import {requestCurrentTime} from './ReactFiberWorkLoop';
12 import {inferPriorityFromExpirationTime} from './ReactFiberExpirationTime';
13
14 import type {Fiber} from './ReactFiber';
packages/react-reconciler/src/ReactFiberHooks.js
+1 -1
@@ -36,7 +36,7 @@ import {
36 requestCurrentTime,
37 warnIfNotCurrentlyActingUpdatesInDev,
38 markRenderEventTimeAndConfig,
39 -} from './ReactFiberScheduler';
39 +} from './ReactFiberWorkLoop';
40
41 import invariant from 'shared/invariant';
42 import warning from 'shared/warning';
packages/react-reconciler/src/ReactFiberHotReloading.js
+1 -1
@@ -15,7 +15,7 @@ import {
15 flushSync,
16 scheduleWork,
17 flushPassiveEffects,
18 -} from './ReactFiberScheduler';
18 +} from './ReactFiberWorkLoop';
19 import {Sync} from './ReactFiberExpirationTime';
20 import {
21 FunctionComponent,
packages/react-reconciler/src/ReactFiberReconciler.js
+1 -1
@@ -56,7 +56,7 @@ import {
56 discreteUpdates,
57 flushDiscreteUpdates,
58 flushPassiveEffects,
59 -} from './ReactFiberScheduler';
59 +} from './ReactFiberWorkLoop';
60 import {createUpdate, enqueueUpdate} from './ReactUpdateQueue';
61 import ReactFiberInstrumentation from './ReactFiberInstrumentation';
62 import {
packages/react-reconciler/src/ReactFiberRoot.js
+1 -1
@@ -11,7 +11,7 @@ import type {Fiber} from './ReactFiber';
11 import type {ExpirationTime} from './ReactFiberExpirationTime';
12 import type {RootTag} from 'shared/ReactRootTags';
13 import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig';
14 -import type {Thenable} from './ReactFiberScheduler';
14 +import type {Thenable} from './ReactFiberWorkLoop';
15 import type {Interaction} from 'scheduler/src/Tracing';
16
17 import {noTimeout} from './ReactFiberHostConfig';
packages/react-reconciler/src/ReactFiberThrow.js new
+383
@@ -0,0 +1,383 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {Fiber} from './ReactFiber';
11 +import type {FiberRoot} from './ReactFiberRoot';
12 +import type {ExpirationTime} from './ReactFiberExpirationTime';
13 +import type {CapturedValue} from './ReactCapturedValue';
14 +import type {Update} from './ReactUpdateQueue';
15 +import type {Thenable} from './ReactFiberWorkLoop';
16 +import type {SuspenseContext} from './ReactFiberSuspenseContext';
17 +
18 +import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
19 +import getComponentName from 'shared/getComponentName';
20 +import warningWithoutStack from 'shared/warningWithoutStack';
21 +import {
22 + ClassComponent,
23 + HostRoot,
24 + SuspenseComponent,
25 + DehydratedSuspenseComponent,
26 + IncompleteClassComponent,
27 +} from 'shared/ReactWorkTags';
28 +import {
29 + DidCapture,
30 + Incomplete,
31 + NoEffect,
32 + ShouldCapture,
33 + LifecycleEffectMask,
34 +} from 'shared/ReactSideEffectTags';
35 +import {
36 + enableSchedulerTracing,
37 + enableSuspenseServerRenderer,
38 +} from 'shared/ReactFeatureFlags';
39 +import {NoMode, BatchedMode} from './ReactTypeOfMode';
40 +import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent';
41 +
42 +import {createCapturedValue} from './ReactCapturedValue';
43 +import {
44 + enqueueCapturedUpdate,
45 + createUpdate,
46 + CaptureUpdate,
47 + ForceUpdate,
48 + enqueueUpdate,
49 +} from './ReactUpdateQueue';
50 +import {logError} from './ReactFiberCommitWork';
51 +import {getStackByFiberInDevAndProd} from './ReactCurrentFiber';
52 +import {markFailedErrorBoundaryForHotReloading} from './ReactFiberHotReloading';
53 +import {
54 + suspenseStackCursor,
55 + InvisibleParentSuspenseContext,
56 + hasSuspenseContext,
57 +} from './ReactFiberSuspenseContext';
58 +import {
59 + renderDidError,
60 + onUncaughtError,
61 + markLegacyErrorBoundaryAsFailed,
62 + isAlreadyFailedLegacyErrorBoundary,
63 + pingSuspendedRoot,
64 + resolveRetryThenable,
65 + checkForWrongSuspensePriorityInDEV,
66 +} from './ReactFiberWorkLoop';
67 +
68 +import invariant from 'shared/invariant';
69 +
70 +import {Sync} from './ReactFiberExpirationTime';
71 +
72 +const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
73 +const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
74 +
75 +function createRootErrorUpdate(
76 + fiber: Fiber,
77 + errorInfo: CapturedValue<mixed>,
78 + expirationTime: ExpirationTime,
79 +): Update<mixed> {
80 + const update = createUpdate(expirationTime, null);
81 + // Unmount the root by rendering null.
82 + update.tag = CaptureUpdate;
83 + // Caution: React DevTools currently depends on this property
84 + // being called "element".
85 + update.payload = {element: null};
86 + const error = errorInfo.value;
87 + update.callback = () => {
88 + onUncaughtError(error);
89 + logError(fiber, errorInfo);
90 + };
91 + return update;
92 +}
93 +
94 +function createClassErrorUpdate(
95 + fiber: Fiber,
96 + errorInfo: CapturedValue<mixed>,
97 + expirationTime: ExpirationTime,
98 +): Update<mixed> {
99 + const update = createUpdate(expirationTime, null);
100 + update.tag = CaptureUpdate;
101 + const getDerivedStateFromError = fiber.type.getDerivedStateFromError;
102 + if (typeof getDerivedStateFromError === 'function') {
103 + const error = errorInfo.value;
104 + update.payload = () => {
105 + return getDerivedStateFromError(error);
106 + };
107 + }
108 +
109 + const inst = fiber.stateNode;
110 + if (inst !== null && typeof inst.componentDidCatch === 'function') {
111 + update.callback = function callback() {
112 + if (__DEV__) {
113 + markFailedErrorBoundaryForHotReloading(fiber);
114 + }
115 + if (typeof getDerivedStateFromError !== 'function') {
116 + // To preserve the preexisting retry behavior of error boundaries,
117 + // we keep track of which ones already failed during this batch.
118 + // This gets reset before we yield back to the browser.
119 + // TODO: Warn in strict mode if getDerivedStateFromError is
120 + // not defined.
121 + markLegacyErrorBoundaryAsFailed(this);
122 + }
123 + const error = errorInfo.value;
124 + const stack = errorInfo.stack;
125 + logError(fiber, errorInfo);
126 + this.componentDidCatch(error, {
127 + componentStack: stack !== null ? stack : '',
128 + });
129 + if (__DEV__) {
130 + if (typeof getDerivedStateFromError !== 'function') {
131 + // If componentDidCatch is the only error boundary method defined,
132 + // then it needs to call setState to recover from errors.
133 + // If no state update is scheduled then the boundary will swallow the error.
134 + warningWithoutStack(
135 + fiber.expirationTime === Sync,
136 + '%s: Error boundaries should implement getDerivedStateFromError(). ' +
137 + 'In that method, return a state update to display an error message or fallback UI.',
138 + getComponentName(fiber.type) || 'Unknown',
139 + );
140 + }
141 + }
142 + };
143 + } else if (__DEV__) {
144 + update.callback = () => {
145 + markFailedErrorBoundaryForHotReloading(fiber);
146 + };
147 + }
148 + return update;
149 +}
150 +
151 +function attachPingListener(
152 + root: FiberRoot,
153 + renderExpirationTime: ExpirationTime,
154 + thenable: Thenable,
155 +) {
156 + // Attach a listener to the promise to "ping" the root and retry. But
157 + // only if one does not already exist for the current render expiration
158 + // time (which acts like a "thread ID" here).
159 + let pingCache = root.pingCache;
160 + let threadIDs;
161 + if (pingCache === null) {
162 + pingCache = root.pingCache = new PossiblyWeakMap();
163 + threadIDs = new Set();
164 + pingCache.set(thenable, threadIDs);
165 + } else {
166 + threadIDs = pingCache.get(thenable);
167 + if (threadIDs === undefined) {
168 + threadIDs = new Set();
169 + pingCache.set(thenable, threadIDs);
170 + }
171 + }
172 + if (!threadIDs.has(renderExpirationTime)) {
173 + // Memoize using the thread ID to prevent redundant listeners.
174 + threadIDs.add(renderExpirationTime);
175 + let ping = pingSuspendedRoot.bind(
176 + null,
177 + root,
178 + thenable,
179 + renderExpirationTime,
180 + );
181 + if (enableSchedulerTracing) {
182 + ping = Schedule_tracing_wrap(ping);
183 + }
184 + thenable.then(ping, ping);
185 + }
186 +}
187 +
188 +function throwException(
189 + root: FiberRoot,
190 + returnFiber: Fiber,
191 + sourceFiber: Fiber,
192 + value: mixed,
193 + renderExpirationTime: ExpirationTime,
194 +) {
195 + // The source fiber did not complete.
196 + sourceFiber.effectTag |= Incomplete;
197 + // Its effect list is no longer valid.
198 + sourceFiber.firstEffect = sourceFiber.lastEffect = null;
199 +
200 + if (
201 + value !== null &&
202 + typeof value === 'object' &&
203 + typeof value.then === 'function'
204 + ) {
205 + // This is a thenable.
206 + const thenable: Thenable = (value: any);
207 +
208 + checkForWrongSuspensePriorityInDEV(sourceFiber);
209 +
210 + let hasInvisibleParentBoundary = hasSuspenseContext(
211 + suspenseStackCursor.current,
212 + (InvisibleParentSuspenseContext: SuspenseContext),
213 + );
214 +
215 + // Schedule the nearest Suspense to re-render the timed out view.
216 + let workInProgress = returnFiber;
217 + do {
218 + if (
219 + workInProgress.tag === SuspenseComponent &&
220 + shouldCaptureSuspense(workInProgress, hasInvisibleParentBoundary)
221 + ) {
222 + // Found the nearest boundary.
223 +
224 + // Stash the promise on the boundary fiber. If the boundary times out, we'll
225 + // attach another listener to flip the boundary back to its normal state.
226 + const thenables: Set<Thenable> = (workInProgress.updateQueue: any);
227 + if (thenables === null) {
228 + const updateQueue = (new Set(): any);
229 + updateQueue.add(thenable);
230 + workInProgress.updateQueue = updateQueue;
231 + } else {
232 + thenables.add(thenable);
233 + }
234 +
235 + // If the boundary is outside of batched mode, we should *not*
236 + // suspend the commit. Pretend as if the suspended component rendered
237 + // null and keep rendering. In the commit phase, we'll schedule a
238 + // subsequent synchronous update to re-render the Suspense.
239 + //
240 + // Note: It doesn't matter whether the component that suspended was
241 + // inside a batched mode tree. If the Suspense is outside of it, we
242 + // should *not* suspend the commit.
243 + if ((workInProgress.mode & BatchedMode) === NoMode) {
244 + workInProgress.effectTag |= DidCapture;
245 +
246 + // We're going to commit this fiber even though it didn't complete.
247 + // But we shouldn't call any lifecycle methods or callbacks. Remove
248 + // all lifecycle effect tags.
249 + sourceFiber.effectTag &= ~(LifecycleEffectMask | Incomplete);
250 +
251 + if (sourceFiber.tag === ClassComponent) {
252 + const currentSourceFiber = sourceFiber.alternate;
253 + if (currentSourceFiber === null) {
254 + // This is a new mount. Change the tag so it's not mistaken for a
255 + // completed class component. For example, we should not call
256 + // componentWillUnmount if it is deleted.
257 + sourceFiber.tag = IncompleteClassComponent;
258 + } else {
259 + // When we try rendering again, we should not reuse the current fiber,
260 + // since it's known to be in an inconsistent state. Use a force updte to
261 + // prevent a bail out.
262 + const update = createUpdate(Sync, null);
263 + update.tag = ForceUpdate;
264 + enqueueUpdate(sourceFiber, update);
265 + }
266 + }
267 +
268 + // The source fiber did not complete. Mark it with Sync priority to
269 + // indicate that it still has pending work.
270 + sourceFiber.expirationTime = Sync;
271 +
272 + // Exit without suspending.
273 + return;
274 + }
275 +
276 + // Confirmed that the boundary is in a concurrent mode tree. Continue
277 + // with the normal suspend path.
278 +
279 + attachPingListener(root, renderExpirationTime, thenable);
280 +
281 + workInProgress.effectTag |= ShouldCapture;
282 + workInProgress.expirationTime = renderExpirationTime;
283 +
284 + return;
285 + } else if (
286 + enableSuspenseServerRenderer &&
287 + workInProgress.tag === DehydratedSuspenseComponent
288 + ) {
289 + attachPingListener(root, renderExpirationTime, thenable);
290 +
291 + // Since we already have a current fiber, we can eagerly add a retry listener.
292 + let retryCache = workInProgress.memoizedState;
293 + if (retryCache === null) {
294 + retryCache = workInProgress.memoizedState = new PossiblyWeakSet();
295 + const current = workInProgress.alternate;
296 + invariant(
297 + current,
298 + 'A dehydrated suspense boundary must commit before trying to render. ' +
299 + 'This is probably a bug in React.',
300 + );
301 + current.memoizedState = retryCache;
302 + }
303 + // Memoize using the boundary fiber to prevent redundant listeners.
304 + if (!retryCache.has(thenable)) {
305 + retryCache.add(thenable);
306 + let retry = resolveRetryThenable.bind(null, workInProgress, thenable);
307 + if (enableSchedulerTracing) {
308 + retry = Schedule_tracing_wrap(retry);
309 + }
310 + thenable.then(retry, retry);
311 + }
312 + workInProgress.effectTag |= ShouldCapture;
313 + workInProgress.expirationTime = renderExpirationTime;
314 + return;
315 + }
316 + // This boundary already captured during this render. Continue to the next
317 + // boundary.
318 + workInProgress = workInProgress.return;
319 + } while (workInProgress !== null);
320 + // No boundary was found. Fallthrough to error mode.
321 + // TODO: Use invariant so the message is stripped in prod?
322 + value = new Error(
323 + (getComponentName(sourceFiber.type) || 'A React component') +
324 + ' suspended while rendering, but no fallback UI was specified.\n' +
325 + '\n' +
326 + 'Add a <Suspense fallback=...> component higher in the tree to ' +
327 + 'provide a loading indicator or placeholder to display.' +
328 + getStackByFiberInDevAndProd(sourceFiber),
329 + );
330 + }
331 +
332 + // We didn't find a boundary that could handle this type of exception. Start
333 + // over and traverse parent path again, this time treating the exception
334 + // as an error.
335 + renderDidError();
336 + value = createCapturedValue(value, sourceFiber);
337 + let workInProgress = returnFiber;
338 + do {
339 + switch (workInProgress.tag) {
340 + case HostRoot: {
341 + const errorInfo = value;
342 + workInProgress.effectTag |= ShouldCapture;
343 + workInProgress.expirationTime = renderExpirationTime;
344 + const update = createRootErrorUpdate(
345 + workInProgress,
346 + errorInfo,
347 + renderExpirationTime,
348 + );
349 + enqueueCapturedUpdate(workInProgress, update);
350 + return;
351 + }
352 + case ClassComponent:
353 + // Capture and retry
354 + const errorInfo = value;
355 + const ctor = workInProgress.type;
356 + const instance = workInProgress.stateNode;
357 + if (
358 + (workInProgress.effectTag & DidCapture) === NoEffect &&
359 + (typeof ctor.getDerivedStateFromError === 'function' ||
360 + (instance !== null &&
361 + typeof instance.componentDidCatch === 'function' &&
362 + !isAlreadyFailedLegacyErrorBoundary(instance)))
363 + ) {
364 + workInProgress.effectTag |= ShouldCapture;
365 + workInProgress.expirationTime = renderExpirationTime;
366 + // Schedule the error boundary to re-render using updated state
367 + const update = createClassErrorUpdate(
368 + workInProgress,
369 + errorInfo,
370 + renderExpirationTime,
371 + );
372 + enqueueCapturedUpdate(workInProgress, update);
373 + return;
374 + }
375 + break;
376 + default:
377 + break;
378 + }
379 + workInProgress = workInProgress.return;
380 + } while (workInProgress !== null);
381 +}
382 +
383 +export {throwException, createRootErrorUpdate, createClassErrorUpdate};
packages/react-reconciler/src/ReactFiberUnwindWork.js
+3 -365
@@ -8,16 +8,8 @@
8 */
9
10 import type {Fiber} from './ReactFiber';
11 -import type {FiberRoot} from './ReactFiberRoot';
11 import type {ExpirationTime} from './ReactFiberExpirationTime';
13 -import type {CapturedValue} from './ReactCapturedValue';
14 -import type {Update} from './ReactUpdateQueue';
15 -import type {Thenable} from './ReactFiberScheduler';
16 -import type {SuspenseContext} from './ReactFiberSuspenseContext';
12
18 -import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
19 -import getComponentName from 'shared/getComponentName';
20 -import warningWithoutStack from 'shared/warningWithoutStack';
13 import {
14 ClassComponent,
15 HostRoot,
@@ -26,374 +18,26 @@ import {
18 ContextProvider,
19 SuspenseComponent,
20 DehydratedSuspenseComponent,
29 - IncompleteClassComponent,
21 EventComponent,
22 EventTarget,
23 } from 'shared/ReactWorkTags';
24 +import {DidCapture, NoEffect, ShouldCapture} from 'shared/ReactSideEffectTags';
25 import {
34 - DidCapture,
35 - Incomplete,
36 - NoEffect,
37 - ShouldCapture,
38 - LifecycleEffectMask,
39 -} from 'shared/ReactSideEffectTags';
40 -import {
41 - enableSchedulerTracing,
26 enableSuspenseServerRenderer,
27 enableEventAPI,
28 } from 'shared/ReactFeatureFlags';
45 -import {NoMode, BatchedMode} from './ReactTypeOfMode';
46 -import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent';
29
48 -import {createCapturedValue} from './ReactCapturedValue';
49 -import {
50 - enqueueCapturedUpdate,
51 - createUpdate,
52 - CaptureUpdate,
53 - ForceUpdate,
54 - enqueueUpdate,
55 -} from './ReactUpdateQueue';
56 -import {logError} from './ReactFiberCommitWork';
57 -import {getStackByFiberInDevAndProd} from './ReactCurrentFiber';
30 import {popHostContainer, popHostContext} from './ReactFiberHostContext';
59 -import {markFailedErrorBoundaryForHotReloading} from './ReactFiberHotReloading';
60 -import {
61 - suspenseStackCursor,
62 - InvisibleParentSuspenseContext,
63 - hasSuspenseContext,
64 - popSuspenseContext,
65 -} from './ReactFiberSuspenseContext';
31 +import {popSuspenseContext} from './ReactFiberSuspenseContext';
32 import {
33 isContextProvider as isLegacyContextProvider,
34 popContext as popLegacyContext,
35 popTopLevelContextObject as popTopLevelLegacyContextObject,
36 } from './ReactFiberContext';
37 import {popProvider} from './ReactFiberNewContext';
72 -import {
73 - renderDidError,
74 - onUncaughtError,
75 - markLegacyErrorBoundaryAsFailed,
76 - isAlreadyFailedLegacyErrorBoundary,
77 - pingSuspendedRoot,
78 - resolveRetryThenable,
79 - checkForWrongSuspensePriorityInDEV,
80 -} from './ReactFiberScheduler';
38
39 import invariant from 'shared/invariant';
40
84 -import {Sync} from './ReactFiberExpirationTime';
85 -
86 -const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
87 -const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
88 -
89 -function createRootErrorUpdate(
90 - fiber: Fiber,
91 - errorInfo: CapturedValue<mixed>,
92 - expirationTime: ExpirationTime,
93 -): Update<mixed> {
94 - const update = createUpdate(expirationTime, null);
95 - // Unmount the root by rendering null.
96 - update.tag = CaptureUpdate;
97 - // Caution: React DevTools currently depends on this property
98 - // being called "element".
99 - update.payload = {element: null};
100 - const error = errorInfo.value;
101 - update.callback = () => {
102 - onUncaughtError(error);
103 - logError(fiber, errorInfo);
104 - };
105 - return update;
106 -}
107 -
108 -function createClassErrorUpdate(
109 - fiber: Fiber,
110 - errorInfo: CapturedValue<mixed>,
111 - expirationTime: ExpirationTime,
112 -): Update<mixed> {
113 - const update = createUpdate(expirationTime, null);
114 - update.tag = CaptureUpdate;
115 - const getDerivedStateFromError = fiber.type.getDerivedStateFromError;
116 - if (typeof getDerivedStateFromError === 'function') {
117 - const error = errorInfo.value;
118 - update.payload = () => {
119 - return getDerivedStateFromError(error);
120 - };
121 - }
122 -
123 - const inst = fiber.stateNode;
124 - if (inst !== null && typeof inst.componentDidCatch === 'function') {
125 - update.callback = function callback() {
126 - if (__DEV__) {
127 - markFailedErrorBoundaryForHotReloading(fiber);
128 - }
129 - if (typeof getDerivedStateFromError !== 'function') {
130 - // To preserve the preexisting retry behavior of error boundaries,
131 - // we keep track of which ones already failed during this batch.
132 - // This gets reset before we yield back to the browser.
133 - // TODO: Warn in strict mode if getDerivedStateFromError is
134 - // not defined.
135 - markLegacyErrorBoundaryAsFailed(this);
136 - }
137 - const error = errorInfo.value;
138 - const stack = errorInfo.stack;
139 - logError(fiber, errorInfo);
140 - this.componentDidCatch(error, {
141 - componentStack: stack !== null ? stack : '',
142 - });
143 - if (__DEV__) {
144 - if (typeof getDerivedStateFromError !== 'function') {
145 - // If componentDidCatch is the only error boundary method defined,
146 - // then it needs to call setState to recover from errors.
147 - // If no state update is scheduled then the boundary will swallow the error.
148 - warningWithoutStack(
149 - fiber.expirationTime === Sync,
150 - '%s: Error boundaries should implement getDerivedStateFromError(). ' +
151 - 'In that method, return a state update to display an error message or fallback UI.',
152 - getComponentName(fiber.type) || 'Unknown',
153 - );
154 - }
155 - }
156 - };
157 - } else if (__DEV__) {
158 - update.callback = () => {
159 - markFailedErrorBoundaryForHotReloading(fiber);
160 - };
161 - }
162 - return update;
163 -}
164 -
165 -function attachPingListener(
166 - root: FiberRoot,
167 - renderExpirationTime: ExpirationTime,
168 - thenable: Thenable,
169 -) {
170 - // Attach a listener to the promise to "ping" the root and retry. But
171 - // only if one does not already exist for the current render expiration
172 - // time (which acts like a "thread ID" here).
173 - let pingCache = root.pingCache;
174 - let threadIDs;
175 - if (pingCache === null) {
176 - pingCache = root.pingCache = new PossiblyWeakMap();
177 - threadIDs = new Set();
178 - pingCache.set(thenable, threadIDs);
179 - } else {
180 - threadIDs = pingCache.get(thenable);
181 - if (threadIDs === undefined) {
182 - threadIDs = new Set();
183 - pingCache.set(thenable, threadIDs);
184 - }
185 - }
186 - if (!threadIDs.has(renderExpirationTime)) {
187 - // Memoize using the thread ID to prevent redundant listeners.
188 - threadIDs.add(renderExpirationTime);
189 - let ping = pingSuspendedRoot.bind(
190 - null,
191 - root,
192 - thenable,
193 - renderExpirationTime,
194 - );
195 - if (enableSchedulerTracing) {
196 - ping = Schedule_tracing_wrap(ping);
197 - }
198 - thenable.then(ping, ping);
199 - }
200 -}
201 -
202 -function throwException(
203 - root: FiberRoot,
204 - returnFiber: Fiber,
205 - sourceFiber: Fiber,
206 - value: mixed,
207 - renderExpirationTime: ExpirationTime,
208 -) {
209 - // The source fiber did not complete.
210 - sourceFiber.effectTag |= Incomplete;
211 - // Its effect list is no longer valid.
212 - sourceFiber.firstEffect = sourceFiber.lastEffect = null;
213 -
214 - if (
215 - value !== null &&
216 - typeof value === 'object' &&
217 - typeof value.then === 'function'
218 - ) {
219 - // This is a thenable.
220 - const thenable: Thenable = (value: any);
221 -
222 - checkForWrongSuspensePriorityInDEV(sourceFiber);
223 -
224 - let hasInvisibleParentBoundary = hasSuspenseContext(
225 - suspenseStackCursor.current,
226 - (InvisibleParentSuspenseContext: SuspenseContext),
227 - );
228 -
229 - // Schedule the nearest Suspense to re-render the timed out view.
230 - let workInProgress = returnFiber;
231 - do {
232 - if (
233 - workInProgress.tag === SuspenseComponent &&
234 - shouldCaptureSuspense(workInProgress, hasInvisibleParentBoundary)
235 - ) {
236 - // Found the nearest boundary.
237 -
238 - // Stash the promise on the boundary fiber. If the boundary times out, we'll
239 - // attach another listener to flip the boundary back to its normal state.
240 - const thenables: Set<Thenable> = (workInProgress.updateQueue: any);
241 - if (thenables === null) {
242 - const updateQueue = (new Set(): any);
243 - updateQueue.add(thenable);
244 - workInProgress.updateQueue = updateQueue;
245 - } else {
246 - thenables.add(thenable);
247 - }
248 -
249 - // If the boundary is outside of batched mode, we should *not*
250 - // suspend the commit. Pretend as if the suspended component rendered
251 - // null and keep rendering. In the commit phase, we'll schedule a
252 - // subsequent synchronous update to re-render the Suspense.
253 - //
254 - // Note: It doesn't matter whether the component that suspended was
255 - // inside a batched mode tree. If the Suspense is outside of it, we
256 - // should *not* suspend the commit.
257 - if ((workInProgress.mode & BatchedMode) === NoMode) {
258 - workInProgress.effectTag |= DidCapture;
259 -
260 - // We're going to commit this fiber even though it didn't complete.
261 - // But we shouldn't call any lifecycle methods or callbacks. Remove
262 - // all lifecycle effect tags.
263 - sourceFiber.effectTag &= ~(LifecycleEffectMask | Incomplete);
264 -
265 - if (sourceFiber.tag === ClassComponent) {
266 - const currentSourceFiber = sourceFiber.alternate;
267 - if (currentSourceFiber === null) {
268 - // This is a new mount. Change the tag so it's not mistaken for a
269 - // completed class component. For example, we should not call
270 - // componentWillUnmount if it is deleted.
271 - sourceFiber.tag = IncompleteClassComponent;
272 - } else {
273 - // When we try rendering again, we should not reuse the current fiber,
274 - // since it's known to be in an inconsistent state. Use a force updte to
275 - // prevent a bail out.
276 - const update = createUpdate(Sync, null);
277 - update.tag = ForceUpdate;
278 - enqueueUpdate(sourceFiber, update);
279 - }
280 - }
281 -
282 - // The source fiber did not complete. Mark it with Sync priority to
283 - // indicate that it still has pending work.
284 - sourceFiber.expirationTime = Sync;
285 -
286 - // Exit without suspending.
287 - return;
288 - }
289 -
290 - // Confirmed that the boundary is in a concurrent mode tree. Continue
291 - // with the normal suspend path.
292 -
293 - attachPingListener(root, renderExpirationTime, thenable);
294 -
295 - workInProgress.effectTag |= ShouldCapture;
296 - workInProgress.expirationTime = renderExpirationTime;
297 -
298 - return;
299 - } else if (
300 - enableSuspenseServerRenderer &&
301 - workInProgress.tag === DehydratedSuspenseComponent
302 - ) {
303 - attachPingListener(root, renderExpirationTime, thenable);
304 -
305 - // Since we already have a current fiber, we can eagerly add a retry listener.
306 - let retryCache = workInProgress.memoizedState;
307 - if (retryCache === null) {
308 - retryCache = workInProgress.memoizedState = new PossiblyWeakSet();
309 - const current = workInProgress.alternate;
310 - invariant(
311 - current,
312 - 'A dehydrated suspense boundary must commit before trying to render. ' +
313 - 'This is probably a bug in React.',
314 - );
315 - current.memoizedState = retryCache;
316 - }
317 - // Memoize using the boundary fiber to prevent redundant listeners.
318 - if (!retryCache.has(thenable)) {
319 - retryCache.add(thenable);
320 - let retry = resolveRetryThenable.bind(null, workInProgress, thenable);
321 - if (enableSchedulerTracing) {
322 - retry = Schedule_tracing_wrap(retry);
323 - }
324 - thenable.then(retry, retry);
325 - }
326 - workInProgress.effectTag |= ShouldCapture;
327 - workInProgress.expirationTime = renderExpirationTime;
328 - return;
329 - }
330 - // This boundary already captured during this render. Continue to the next
331 - // boundary.
332 - workInProgress = workInProgress.return;
333 - } while (workInProgress !== null);
334 - // No boundary was found. Fallthrough to error mode.
335 - // TODO: Use invariant so the message is stripped in prod?
336 - value = new Error(
337 - (getComponentName(sourceFiber.type) || 'A React component') +
338 - ' suspended while rendering, but no fallback UI was specified.\n' +
339 - '\n' +
340 - 'Add a <Suspense fallback=...> component higher in the tree to ' +
341 - 'provide a loading indicator or placeholder to display.' +
342 - getStackByFiberInDevAndProd(sourceFiber),
343 - );
344 - }
345 -
346 - // We didn't find a boundary that could handle this type of exception. Start
347 - // over and traverse parent path again, this time treating the exception
348 - // as an error.
349 - renderDidError();
350 - value = createCapturedValue(value, sourceFiber);
351 - let workInProgress = returnFiber;
352 - do {
353 - switch (workInProgress.tag) {
354 - case HostRoot: {
355 - const errorInfo = value;
356 - workInProgress.effectTag |= ShouldCapture;
357 - workInProgress.expirationTime = renderExpirationTime;
358 - const update = createRootErrorUpdate(
359 - workInProgress,
360 - errorInfo,
361 - renderExpirationTime,
362 - );
363 - enqueueCapturedUpdate(workInProgress, update);
364 - return;
365 - }
366 - case ClassComponent:
367 - // Capture and retry
368 - const errorInfo = value;
369 - const ctor = workInProgress.type;
370 - const instance = workInProgress.stateNode;
371 - if (
372 - (workInProgress.effectTag & DidCapture) === NoEffect &&
373 - (typeof ctor.getDerivedStateFromError === 'function' ||
374 - (instance !== null &&
375 - typeof instance.componentDidCatch === 'function' &&
376 - !isAlreadyFailedLegacyErrorBoundary(instance)))
377 - ) {
378 - workInProgress.effectTag |= ShouldCapture;
379 - workInProgress.expirationTime = renderExpirationTime;
380 - // Schedule the error boundary to re-render using updated state
381 - const update = createClassErrorUpdate(
382 - workInProgress,
383 - errorInfo,
384 - renderExpirationTime,
385 - );
386 - enqueueCapturedUpdate(workInProgress, update);
387 - return;
388 - }
389 - break;
390 - default:
391 - break;
392 - }
393 - workInProgress = workInProgress.return;
394 - } while (workInProgress !== null);
395 -}
396 -
41 function unwindWork(
42 workInProgress: Fiber,
43 renderExpirationTime: ExpirationTime,
@@ -512,10 +156,4 @@ function unwindInterruptedWork(interruptedWork: Fiber) {
156 }
157 }
158
515 -export {
516 - throwException,
517 - unwindWork,
518 - unwindInterruptedWork,
519 - createRootErrorUpdate,
520 - createClassErrorUpdate,
521 -};
159 +export {unwindWork, unwindInterruptedWork};
packages/react-reconciler/src/ReactFiberWorkLoop.js renamed
+2 -3
@@ -104,13 +104,12 @@ import {
104 } from './ReactFiberExpirationTime';
105 import {beginWork as originalBeginWork} from './ReactFiberBeginWork';
106 import {completeWork} from './ReactFiberCompleteWork';
107 +import {unwindWork, unwindInterruptedWork} from './ReactFiberUnwindWork';
108 import {
109 throwException,
109 - unwindWork,
110 - unwindInterruptedWork,
110 createRootErrorUpdate,
111 createClassErrorUpdate,
113 -} from './ReactFiberUnwindWork';
112 +} from './ReactFiberThrow';
113 import {
114 commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber,
115 commitLifeCycles as commitLayoutEffectOnFiber,
packages/react-reconciler/src/ReactUpdateQueue.js
+1 -1
@@ -102,7 +102,7 @@ import {
102 } from 'shared/ReactFeatureFlags';
103
104 import {StrictMode} from './ReactTypeOfMode';
105 -import {markRenderEventTimeAndConfig} from './ReactFiberScheduler';
105 +import {markRenderEventTimeAndConfig} from './ReactFiberWorkLoop';
106
107 import invariant from 'shared/invariant';
108 import warningWithoutStack from 'shared/warningWithoutStack';
packages/react-test-renderer/src/ReactTestRendererAct.js
+1 -1
@@ -6,7 +6,7 @@
6 *
7 * @flow
8 */
9 -import type {Thenable} from 'react-reconciler/src/ReactFiberScheduler';
9 +import type {Thenable} from 'react-reconciler/src/ReactFiberWorkLoop';
10
11 import {
12 batchedUpdates,