@samitouri / QOS-React / commits / 94518b068b

Add stack unwinding phase for handling errors (#12201)

* Add stack unwinding phase for handling errors A rewrite of error handling, with semantics that more closely match stack unwinding. Errors that are thrown during the render phase unwind to the nearest error boundary, like before. But rather than synchronously unmount the children before retrying, we restart the failed subtree within the same render phase. The failed children are still unmounted (as if all their keys changed) but without an extra commit. Commit phase errors are different. They work by scheduling an error on the update queue of the error boundary. When we enter the render phase, the error is popped off the queue. The rest of the algorithm is the same. This approach is designed to work for throwing non-errors, too, though that feature is not implemented yet. * Add experimental getDerivedStateFromCatch lifecycle Fires during the render phase, so you can recover from an error within the same pass. This aligns error boundaries more closely with try-catch semantics. Let's keep this behind a feature flag until a future release. For now, the recommendation is to keep using componentDidCatch. Eventually, the advice will be to use getDerivedStateFromCatch for handling errors and componentDidCatch only for logging. * Reconcile twice to remount failed children, instead of using a boolean * Handle effect immediately after its thrown This way we don't have to store the thrown values on the effect list. * ReactFiberIncompleteWork -> ReactFiberUnwindWork * Remove startTime * Remove TypeOfException We don't need it yet. We'll reconsider once we add another exception type. * Move replay to outer catch block This moves it out of the hot path.

Andrew Clark committed Feb 23, 2018 at 17:38 UTC 94518b068bf52196abea0c83f4c9926bfe2065c6
39 files changed +1663 -1052
packages/react-dom/src/__tests__/ReactDOM-test.js
+5
@@ -380,6 +380,7 @@ describe('ReactDOM', () => {
380 it('throws in DEV if jsdom is destroyed by the time setState() is called', () => {
381 class App extends React.Component {
382 state = {x: 1};
383 + componentDidUpdate() {}
384 render() {
385 return <div />;
386 }
@@ -395,6 +396,10 @@ describe('ReactDOM', () => {
396 // This is roughly what happens if the test finished and then
397 // an asynchronous callback tried to setState() after this.
398 delete global.document;
399 +
400 + // The error we're interested in is thrown by invokeGuardedCallback, which
401 + // in DEV is used 1) to replay a failed begin phase, or 2) when calling
402 + // lifecycle methods. We're triggering the second case here.
403 const fn = () => instance.setState({x: 2});
404 if (__DEV__) {
405 expect(fn).toThrow(
packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js renamed
+24 -2
@@ -12,6 +12,7 @@
12 let PropTypes;
13 let React;
14 let ReactDOM;
15 +let ReactFeatureFlags;
16
17 describe('ReactErrorBoundaries', () => {
18 let log;
@@ -34,7 +35,10 @@ describe('ReactErrorBoundaries', () => {
35 let Normal;
36
37 beforeEach(() => {
38 + jest.resetModules();
39 PropTypes = require('prop-types');
40 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
41 + ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
42 ReactDOM = require('react-dom');
43 React = require('react');
44
@@ -922,6 +926,10 @@ describe('ReactErrorBoundaries', () => {
926 'BrokenRender constructor',
927 'BrokenRender componentWillMount',
928 'BrokenRender render [!]',
929 + // Render third child, even though an earlier sibling threw.
930 + 'Normal constructor',
931 + 'Normal componentWillMount',
932 + 'Normal render',
933 // Finish mounting with null children
934 'ErrorBoundary componentDidMount',
935 // Handle the error
@@ -1379,6 +1387,10 @@ describe('ReactErrorBoundaries', () => {
1387 // The initial render was aborted, so
1388 // Fiber retries from the root.
1389 'ErrorBoundary componentWillUpdate',
1390 + 'ErrorBoundary componentDidUpdate',
1391 + // The second willUnmount error should be captured and logged, too.
1392 + 'ErrorBoundary componentDidCatch',
1393 + 'ErrorBoundary componentWillUpdate',
1394 // Render an error now (stack will do it later)
1395 'ErrorBoundary render error',
1396 // Attempt to unmount previous child:
@@ -1437,6 +1449,10 @@ describe('ReactErrorBoundaries', () => {
1449 // The initial render was aborted, so
1450 // Fiber retries from the root.
1451 'ErrorBoundary componentWillUpdate',
1452 + 'ErrorBoundary componentDidUpdate',
1453 + // The second willUnmount error should be captured and logged, too.
1454 + 'ErrorBoundary componentDidCatch',
1455 + 'ErrorBoundary componentWillUpdate',
1456 // Render an error now (stack will do it later)
1457 'ErrorBoundary render error',
1458 // Done
@@ -1761,6 +1777,10 @@ describe('ReactErrorBoundaries', () => {
1777 // Handle the error
1778 'ErrorBoundary componentDidCatch',
1779 'ErrorBoundary componentWillUpdate',
1780 + // The willUnmount error should be captured and logged, too.
1781 + 'ErrorBoundary componentDidUpdate',
1782 + 'ErrorBoundary componentDidCatch',
1783 + 'ErrorBoundary componentWillUpdate',
1784 'ErrorBoundary render error',
1785 // The update has finished
1786 'ErrorBoundary componentDidUpdate',
@@ -1847,7 +1867,7 @@ describe('ReactErrorBoundaries', () => {
1867 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1868 });
1869
1850 - it('lets different boundaries catch their own first errors', () => {
1870 + it('calls componentDidCatch for each error that is captured', () => {
1871 function renderUnmountError(error) {
1872 return <div>Caught an unmounting error: {error.message}.</div>;
1873 }
@@ -1892,7 +1912,7 @@ describe('ReactErrorBoundaries', () => {
1912 );
1913
1914 expect(container.firstChild.textContent).toBe(
1895 - 'Caught an unmounting error: E1.' + 'Caught an updating error: E3.',
1915 + 'Caught an unmounting error: E2.' + 'Caught an updating error: E4.',
1916 );
1917 expect(log).toEqual([
1918 // Begin update phase
@@ -1928,6 +1948,8 @@ describe('ReactErrorBoundaries', () => {
1948 'BrokenComponentDidUpdate componentWillUnmount',
1949 'BrokenComponentDidUpdate componentWillUnmount',
1950 'InnerUnmountBoundary componentDidCatch',
1951 + 'InnerUnmountBoundary componentDidCatch',
1952 + 'InnerUpdateBoundary componentDidCatch',
1953 'InnerUpdateBoundary componentDidCatch',
1954 'InnerUnmountBoundary componentWillUpdate',
1955 'InnerUnmountBoundary render error',
packages/react-noop-renderer/src/ReactNoop.js
+8 -2
@@ -287,7 +287,6 @@ function* flushUnitsOfWork(n: number): Generator<Array<mixed>, void, void> {
287 while (!didStop && scheduledCallback !== null) {
288 let cb = scheduledCallback;
289 scheduledCallback = null;
290 - yieldedValues = null;
290 unitsRemaining = n;
291 cb({
292 timeRemaining() {
@@ -418,7 +417,8 @@ const ReactNoop = {
417 },
418
419 flushUnitsOfWork(n: number): Array<mixed> {
421 - let values = [];
420 + let values = yieldedValues || [];
421 + yieldedValues = null;
422 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
423 for (const value of flushUnitsOfWork(n)) {
424 values.push(...value);
@@ -456,6 +456,12 @@ const ReactNoop = {
456 }
457 },
458
459 + clearYields() {
460 + const values = yieldedValues;
461 + yieldedValues = null;
462 + return values;
463 + },
464 +
465 hasScheduledCallback() {
466 return !!scheduledCallback;
467 },
packages/react-reconciler/src/ReactCapturedValue.js new
+41
@@ -0,0 +1,41 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
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 +
12 +import {getStackAddendumByWorkInProgressFiber} from 'shared/ReactFiberComponentTreeHook';
13 +
14 +export type CapturedValue<T> = {
15 + value: T,
16 + source: Fiber | null,
17 + stack: string | null,
18 +};
19 +
20 +export type CapturedError = {
21 + componentName: ?string,
22 + componentStack: string,
23 + error: mixed,
24 + errorBoundary: ?Object,
25 + errorBoundaryFound: boolean,
26 + errorBoundaryName: string | null,
27 + willRetry: boolean,
28 +};
29 +
30 +export function createCapturedValue<T>(
31 + value: T,
32 + source: Fiber | null,
33 +): CapturedValue<T> {
34 + // If the value is an error, call this function immediately after it is thrown
35 + // so the stack is accurate.
36 + return {
37 + value,
38 + source,
39 + stack: getStackAddendumByWorkInProgressFiber(source),
40 + };
41 +}
packages/react-reconciler/src/ReactFiberBeginWork.js
+94 -96
@@ -35,11 +35,11 @@ import {
35 PerformedWork,
36 Placement,
37 ContentReset,
38 - Err,
38 Ref,
39 } from 'shared/ReactTypeOfSideEffect';
40 import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState';
41 import {
42 + enableGetDerivedStateFromCatch,
43 debugRenderPhaseSideEffects,
44 debugRenderPhaseSideEffectsForStrictMode,
45 } from 'shared/ReactFeatureFlags';
@@ -101,7 +101,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
101 callGetDerivedStateFromProps,
102 constructClassInstance,
103 mountClassInstance,
104 - // resumeMountClassInstance,
104 + resumeMountClassInstance,
105 updateClassInstance,
106 } = ReactFiberClassComponent(
107 scheduleWork,
@@ -237,19 +237,20 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
237 // During mounting we don't know the child context yet as the instance doesn't exist.
238 // We will invalidate the child context in finishClassComponent() right after rendering.
239 const hasContext = pushLegacyContextProvider(workInProgress);
240 -
240 let shouldUpdate;
241 if (current === null) {
243 - if (!workInProgress.stateNode) {
242 + if (workInProgress.stateNode === null) {
243 // In the initial pass we might need to construct the instance.
244 constructClassInstance(workInProgress, workInProgress.pendingProps);
245 mountClassInstance(workInProgress, renderExpirationTime);
246
247 shouldUpdate = true;
248 } else {
250 - invariant(false, 'Resuming work not yet implemented.');
249 // In a resume, we'll already have an instance we can reuse.
252 - // shouldUpdate = resumeMountClassInstance(workInProgress, renderExpirationTime);
250 + shouldUpdate = resumeMountClassInstance(
251 + workInProgress,
252 + renderExpirationTime,
253 + );
254 }
255 } else {
256 shouldUpdate = updateClassInstance(
@@ -258,11 +259,23 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
259 renderExpirationTime,
260 );
261 }
262 +
263 + // We processed the update queue inside updateClassInstance. It may have
264 + // included some errors that were dispatched during the commit phase.
265 + // TODO: Refactor class components so this is less awkward.
266 + let didCaptureError = false;
267 + const updateQueue = workInProgress.updateQueue;
268 + if (updateQueue !== null && updateQueue.capturedValues !== null) {
269 + shouldUpdate = true;
270 + didCaptureError = true;
271 + }
272 return finishClassComponent(
273 current,
274 workInProgress,
275 shouldUpdate,
276 hasContext,
277 + didCaptureError,
278 + renderExpirationTime,
279 );
280 }
281
@@ -271,11 +284,13 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
284 workInProgress: Fiber,
285 shouldUpdate: boolean,
286 hasContext: boolean,
287 + didCaptureError: boolean,
288 + renderExpirationTime: ExpirationTime,
289 ) {
290 // Refs should update even if shouldComponentUpdate returns false
291 markRef(current, workInProgress);
292
278 - if (!shouldUpdate) {
293 + if (!shouldUpdate && !didCaptureError) {
294 // Context providers should defer to sCU for rendering
295 if (hasContext) {
296 invalidateContextProvider(workInProgress, false);
@@ -284,35 +299,69 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
299 return bailoutOnAlreadyFinishedWork(current, workInProgress);
300 }
301
302 + const ctor = workInProgress.type;
303 const instance = workInProgress.stateNode;
304
305 // Rerender
306 ReactCurrentOwner.current = workInProgress;
307 let nextChildren;
292 - if (__DEV__) {
293 - ReactDebugCurrentFiber.setCurrentPhase('render');
294 - nextChildren = instance.render();
295 - if (
296 - debugRenderPhaseSideEffects ||
297 - (debugRenderPhaseSideEffectsForStrictMode &&
298 - workInProgress.mode & StrictMode)
299 - ) {
300 - instance.render();
301 - }
302 - ReactDebugCurrentFiber.setCurrentPhase(null);
308 + if (
309 + didCaptureError &&
310 + (!enableGetDerivedStateFromCatch ||
311 + typeof ctor.getDerivedStateFromCatch !== 'function')
312 + ) {
313 + // If we captured an error, but getDerivedStateFrom catch is not defined,
314 + // unmount all the children. componentDidCatch will schedule an update to
315 + // re-render a fallback. This is temporary until we migrate everyone to
316 + // the new API.
317 + // TODO: Warn in a future release.
318 + nextChildren = null;
319 } else {
304 - if (
305 - debugRenderPhaseSideEffects ||
306 - (debugRenderPhaseSideEffectsForStrictMode &&
307 - workInProgress.mode & StrictMode)
308 - ) {
309 - instance.render();
320 + if (__DEV__) {
321 + ReactDebugCurrentFiber.setCurrentPhase('render');
322 + nextChildren = instance.render();
323 + if (
324 + debugRenderPhaseSideEffects ||
325 + (debugRenderPhaseSideEffectsForStrictMode &&
326 + workInProgress.mode & StrictMode)
327 + ) {
328 + instance.render();
329 + }
330 + ReactDebugCurrentFiber.setCurrentPhase(null);
331 + } else {
332 + if (
333 + debugRenderPhaseSideEffects ||
334 + (debugRenderPhaseSideEffectsForStrictMode &&
335 + workInProgress.mode & StrictMode)
336 + ) {
337 + instance.render();
338 + }
339 + nextChildren = instance.render();
340 }
311 - nextChildren = instance.render();
341 }
342 +
343 // React DevTools reads this flag.
344 workInProgress.effectTag |= PerformedWork;
315 - reconcileChildren(current, workInProgress, nextChildren);
345 + if (didCaptureError) {
346 + // If we're recovering from an error, reconcile twice: first to delete
347 + // all the existing children.
348 + reconcileChildrenAtExpirationTime(
349 + current,
350 + workInProgress,
351 + null,
352 + renderExpirationTime,
353 + );
354 + workInProgress.child = null;
355 + // Now we can continue reconciling like normal. This has the effect of
356 + // remounting all children regardless of whether their their
357 + // identity matches.
358 + }
359 + reconcileChildrenAtExpirationTime(
360 + current,
361 + workInProgress,
362 + nextChildren,
363 + renderExpirationTime,
364 + );
365 // Memoize props and state using the values we just used to render.
366 // TODO: Restructure so we never read values from the instance.
367 memoizeState(workInProgress, instance.state);
@@ -343,7 +392,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
392
393 function updateHostRoot(current, workInProgress, renderExpirationTime) {
394 pushHostRootContext(workInProgress);
346 - const updateQueue = workInProgress.updateQueue;
395 + let updateQueue = workInProgress.updateQueue;
396 if (updateQueue !== null) {
397 const prevState = workInProgress.memoizedState;
398 const state = processUpdateQueue(
@@ -354,13 +403,21 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
403 null,
404 renderExpirationTime,
405 );
357 - if (prevState === state) {
406 + memoizeState(workInProgress, state);
407 + updateQueue = workInProgress.updateQueue;
408 +
409 + let element;
410 + if (updateQueue !== null && updateQueue.capturedValues !== null) {
411 + // There's an uncaught error. Unmount the whole root.
412 + element = null;
413 + } else if (prevState === state) {
414 // If the state is the same as before, that's a bailout because we had
415 // no work that expires at this time.
416 resetHydrationState();
417 return bailoutOnAlreadyFinishedWork(current, workInProgress);
418 + } else {
419 + element = state.element;
420 }
363 - const element = state.element;
421 const root: FiberRoot = workInProgress.stateNode;
422 if (
423 (current === null || current.child === null) &&
@@ -541,7 +598,14 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
598 const hasContext = pushLegacyContextProvider(workInProgress);
599 adoptClassInstance(workInProgress, value);
600 mountClassInstance(workInProgress, renderExpirationTime);
544 - return finishClassComponent(current, workInProgress, true, hasContext);
601 + return finishClassComponent(
602 + current,
603 + workInProgress,
604 + true,
605 + hasContext,
606 + false,
607 + renderExpirationTime,
608 + );
609 } else {
610 // Proceed under the assumption that this is a functional component
611 workInProgress.tag = FunctionalComponent;
@@ -1047,73 +1111,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1111 }
1112 }
1113
1050 - function beginFailedWork(
1051 - current: Fiber | null,
1052 - workInProgress: Fiber,
1053 - renderExpirationTime: ExpirationTime,
1054 - ) {
1055 - // Push context providers here to avoid a push/pop context mismatch.
1056 - switch (workInProgress.tag) {
1057 - case ClassComponent:
1058 - pushLegacyContextProvider(workInProgress);
1059 - break;
1060 - case HostRoot:
1061 - pushHostRootContext(workInProgress);
1062 - break;
1063 - default:
1064 - invariant(
1065 - false,
1066 - 'Invalid type of work. This error is likely caused by a bug in React. ' +
1067 - 'Please file an issue.',
1068 - );
1069 - }
1070 -
1071 - // Add an error effect so we can handle the error during the commit phase
1072 - workInProgress.effectTag |= Err;
1073 -
1074 - // This is a weird case where we do "resume" work — work that failed on
1075 - // our first attempt. Because we no longer have a notion of "progressed
1076 - // deletions," reset the child to the current child to make sure we delete
1077 - // it again. TODO: Find a better way to handle this, perhaps during a more
1078 - // general overhaul of error handling.
1079 - if (current === null) {
1080 - workInProgress.child = null;
1081 - } else if (workInProgress.child !== current.child) {
1082 - workInProgress.child = current.child;
1083 - }
1084 -
1085 - if (
1086 - workInProgress.expirationTime === NoWork ||
1087 - workInProgress.expirationTime > renderExpirationTime
1088 - ) {
1089 - return bailoutOnLowPriority(current, workInProgress);
1090 - }
1091 -
1092 - // If we don't bail out, we're going be recomputing our children so we need
1093 - // to drop our effect list.
1094 - workInProgress.firstEffect = null;
1095 - workInProgress.lastEffect = null;
1096 -
1097 - // Unmount the current children as if the component rendered null
1098 - const nextChildren = null;
1099 - reconcileChildrenAtExpirationTime(
1100 - current,
1101 - workInProgress,
1102 - nextChildren,
1103 - renderExpirationTime,
1104 - );
1105 -
1106 - if (workInProgress.tag === ClassComponent) {
1107 - const instance = workInProgress.stateNode;
1108 - workInProgress.memoizedProps = instance.props;
1109 - workInProgress.memoizedState = instance.state;
1110 - }
1111 -
1112 - return workInProgress.child;
1113 - }
1114 -
1114 return {
1115 beginWork,
1117 - beginFailedWork,
1116 };
1117 }
packages/react-reconciler/src/ReactFiberClassComponent.js
+225 -119
@@ -9,9 +9,11 @@
9
10 import type {Fiber} from './ReactFiber';
11 import type {ExpirationTime} from './ReactFiberExpirationTime';
12 +import type {CapturedValue} from './ReactCapturedValue';
13
14 import {Update} from 'shared/ReactTypeOfSideEffect';
15 import {
16 + enableGetDerivedStateFromCatch,
17 debugRenderPhaseSideEffects,
18 debugRenderPhaseSideEffectsForStrictMode,
19 warnAboutDeprecatedLifecycles,
@@ -94,6 +96,18 @@ if (__DEV__) {
96 });
97 Object.freeze(fakeInternalInstance);
98 }
99 +function callGetDerivedStateFromCatch(ctor: any, capturedValues: Array<mixed>) {
100 + const resultState = {};
101 + for (let i = 0; i < capturedValues.length; i++) {
102 + const capturedValue: CapturedValue<mixed> = (capturedValues[i]: any);
103 + const error = capturedValue.value;
104 + const partialState = ctor.getDerivedStateFromCatch.call(null, error);
105 + if (partialState !== null && partialState !== undefined) {
106 + Object.assign(resultState, partialState);
107 + }
108 + }
109 + return resultState;
110 +}
111
112 export default function(
113 scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void,
@@ -117,7 +131,7 @@ export default function(
131 callback,
132 isReplace: false,
133 isForced: false,
120 - nextCallback: null,
134 + capturedValue: null,
135 next: null,
136 };
137 insertUpdateIntoFiber(fiber, update);
@@ -136,7 +150,7 @@ export default function(
150 callback,
151 isReplace: true,
152 isForced: false,
139 - nextCallback: null,
153 + capturedValue: null,
154 next: null,
155 };
156 insertUpdateIntoFiber(fiber, update);
@@ -155,7 +169,7 @@ export default function(
169 callback,
170 isReplace: false,
171 isForced: true,
158 - nextCallback: null,
172 + capturedValue: null,
173 next: null,
174 };
175 insertUpdateIntoFiber(fiber, update);
@@ -181,7 +195,7 @@ export default function(
195 }
196
197 const instance = workInProgress.stateNode;
184 - const type = workInProgress.type;
198 + const ctor = workInProgress.type;
199 if (typeof instance.shouldComponentUpdate === 'function') {
200 startPhaseTimer(workInProgress, 'shouldComponentUpdate');
201 const shouldUpdate = instance.shouldComponentUpdate(
@@ -203,7 +217,7 @@ export default function(
217 return shouldUpdate;
218 }
219
206 - if (type.prototype && type.prototype.isPureReactComponent) {
220 + if (ctor.prototype && ctor.prototype.isPureReactComponent) {
221 return (
222 !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
223 );
@@ -359,7 +373,7 @@ export default function(
373 }
374 if (typeof instance.getChildContext === 'function') {
375 warning(
362 - typeof workInProgress.type.childContextTypes === 'object',
376 + typeof type.childContextTypes === 'object',
377 '%s.getChildContext(): childContextTypes must be defined in order to ' +
378 'use getChildContext().',
379 getComponentName(workInProgress),
@@ -587,6 +601,7 @@ export default function(
601 workInProgress: Fiber,
602 renderExpirationTime: ExpirationTime,
603 ): void {
604 + const ctor = workInProgress.type;
605 const current = workInProgress.alternate;
606
607 if (__DEV__) {
@@ -623,7 +638,7 @@ export default function(
638 if (
639 (typeof instance.UNSAFE_componentWillMount === 'function' ||
640 typeof instance.componentWillMount === 'function') &&
626 - typeof workInProgress.type.getDerivedStateFromProps !== 'function'
641 + typeof ctor.getDerivedStateFromProps !== 'function'
642 ) {
643 callComponentWillMount(workInProgress, instance);
644 // If we had additional state updates during this life-cycle, let's
@@ -645,110 +660,172 @@ export default function(
660 }
661 }
662
648 - // Called on a preexisting class instance. Returns false if a resumed render
649 - // could be reused.
650 - // function resumeMountClassInstance(
651 - // workInProgress: Fiber,
652 - // priorityLevel: PriorityLevel,
653 - // ): boolean {
654 - // const instance = workInProgress.stateNode;
655 - // resetInputPointers(workInProgress, instance);
656 -
657 - // let newState = workInProgress.memoizedState;
658 - // let newProps = workInProgress.pendingProps;
659 - // if (!newProps) {
660 - // // If there isn't any new props, then we'll reuse the memoized props.
661 - // // This could be from already completed work.
662 - // newProps = workInProgress.memoizedProps;
663 - // invariant(
664 - // newProps != null,
665 - // 'There should always be pending or memoized props. This error is ' +
666 - // 'likely caused by a bug in React. Please file an issue.',
667 - // );
668 - // }
669 - // const newUnmaskedContext = getUnmaskedContext(workInProgress);
670 - // const newContext = getMaskedContext(workInProgress, newUnmaskedContext);
671 -
672 - // const oldContext = instance.context;
673 - // const oldProps = workInProgress.memoizedProps;
674 -
675 - // if (
676 - // typeof instance.componentWillReceiveProps === 'function' &&
677 - // (oldProps !== newProps || oldContext !== newContext)
678 - // ) {
679 - // callComponentWillReceiveProps(
680 - // workInProgress,
681 - // instance,
682 - // newProps,
683 - // newContext,
684 - // );
685 - // }
686 -
687 - // // Process the update queue before calling shouldComponentUpdate
688 - // const updateQueue = workInProgress.updateQueue;
689 - // if (updateQueue !== null) {
690 - // newState = processUpdateQueue(
691 - // workInProgress,
692 - // updateQueue,
693 - // instance,
694 - // newState,
695 - // newProps,
696 - // priorityLevel,
697 - // );
698 - // }
699 -
700 - // // TODO: Should we deal with a setState that happened after the last
701 - // // componentWillMount and before this componentWillMount? Probably
702 - // // unsupported anyway.
703 -
704 - // if (
705 - // !checkShouldComponentUpdate(
706 - // workInProgress,
707 - // workInProgress.memoizedProps,
708 - // newProps,
709 - // workInProgress.memoizedState,
710 - // newState,
711 - // newContext,
712 - // )
713 - // ) {
714 - // // Update the existing instance's state, props, and context pointers even
715 - // // though we're bailing out.
716 - // instance.props = newProps;
717 - // instance.state = newState;
718 - // instance.context = newContext;
719 - // return false;
720 - // }
721 -
722 - // // Update the input pointers now so that they are correct when we call
723 - // // componentWillMount
724 - // instance.props = newProps;
725 - // instance.state = newState;
726 - // instance.context = newContext;
727 -
728 - // if (typeof instance.componentWillMount === 'function') {
729 - // callComponentWillMount(workInProgress, instance);
730 - // // componentWillMount may have called setState. Process the update queue.
731 - // const newUpdateQueue = workInProgress.updateQueue;
732 - // if (newUpdateQueue !== null) {
733 - // newState = processUpdateQueue(
734 - // workInProgress,
735 - // newUpdateQueue,
736 - // instance,
737 - // newState,
738 - // newProps,
739 - // priorityLevel,
740 - // );
741 - // }
742 - // }
743 -
744 - // if (typeof instance.componentDidMount === 'function') {
745 - // workInProgress.effectTag |= Update;
746 - // }
747 -
748 - // instance.state = newState;
749 -
750 - // return true;
751 - // }
663 + function resumeMountClassInstance(
664 + workInProgress: Fiber,
665 + renderExpirationTime: ExpirationTime,
666 + ): boolean {
667 + const ctor = workInProgress.type;
668 + const instance = workInProgress.stateNode;
669 + resetInputPointers(workInProgress, instance);
670 +
671 + const oldProps = workInProgress.memoizedProps;
672 + const newProps = workInProgress.pendingProps;
673 + const oldContext = instance.context;
674 + const newUnmaskedContext = getUnmaskedContext(workInProgress);
675 + const newContext = getMaskedContext(workInProgress, newUnmaskedContext);
676 +
677 + // Note: During these life-cycles, instance.props/instance.state are what
678 + // ever the previously attempted to render - not the "current". However,
679 + // during componentDidUpdate we pass the "current" props.
680 +
681 + // In order to support react-lifecycles-compat polyfilled components,
682 + // Unsafe lifecycles should not be invoked for any component with the new gDSFP.
683 + if (
684 + (typeof instance.UNSAFE_componentWillReceiveProps === 'function' ||
685 + typeof instance.componentWillReceiveProps === 'function') &&
686 + typeof ctor.getDerivedStateFromProps !== 'function'
687 + ) {
688 + if (oldProps !== newProps || oldContext !== newContext) {
689 + callComponentWillReceiveProps(
690 + workInProgress,
691 + instance,
692 + newProps,
693 + newContext,
694 + );
695 + }
696 + }
697 +
698 + let derivedStateFromProps;
699 + if (oldProps !== newProps) {
700 + derivedStateFromProps = callGetDerivedStateFromProps(
701 + workInProgress,
702 + instance,
703 + newProps,
704 + );
705 + }
706 +
707 + // Compute the next state using the memoized state and the update queue.
708 + const oldState = workInProgress.memoizedState;
709 + // TODO: Previous state can be null.
710 + let newState;
711 + let derivedStateFromCatch;
712 + if (workInProgress.updateQueue !== null) {
713 + newState = processUpdateQueue(
714 + null,
715 + workInProgress,
716 + workInProgress.updateQueue,
717 + instance,
718 + newProps,
719 + renderExpirationTime,
720 + );
721 +
722 + let updateQueue = workInProgress.updateQueue;
723 + if (
724 + updateQueue !== null &&
725 + updateQueue.capturedValues !== null &&
726 + (enableGetDerivedStateFromCatch &&
727 + typeof ctor.getDerivedStateFromCatch === 'function')
728 + ) {
729 + const capturedValues = updateQueue.capturedValues;
730 + // Don't remove these from the update queue yet. We need them in
731 + // finishClassComponent. Do the reset there.
732 + // TODO: This is awkward. Refactor class components.
733 + // updateQueue.capturedValues = null;
734 + derivedStateFromCatch = callGetDerivedStateFromCatch(
735 + ctor,
736 + capturedValues,
737 + );
738 + }
739 + } else {
740 + newState = oldState;
741 + }
742 +
743 + if (derivedStateFromProps !== null && derivedStateFromProps !== undefined) {
744 + // Render-phase updates (like this) should not be added to the update queue,
745 + // So that multiple render passes do not enqueue multiple updates.
746 + // Instead, just synchronously merge the returned state into the instance.
747 + newState =
748 + newState === null || newState === undefined
749 + ? derivedStateFromProps
750 + : Object.assign({}, newState, derivedStateFromProps);
751 + }
752 + if (derivedStateFromCatch !== null && derivedStateFromCatch !== undefined) {
753 + // Render-phase updates (like this) should not be added to the update queue,
754 + // So that multiple render passes do not enqueue multiple updates.
755 + // Instead, just synchronously merge the returned state into the instance.
756 + newState =
757 + newState === null || newState === undefined
758 + ? derivedStateFromCatch
759 + : Object.assign({}, newState, derivedStateFromCatch);
760 + }
761 +
762 + if (
763 + oldProps === newProps &&
764 + oldState === newState &&
765 + !hasContextChanged() &&
766 + !(
767 + workInProgress.updateQueue !== null &&
768 + workInProgress.updateQueue.hasForceUpdate
769 + )
770 + ) {
771 + // If an update was already in progress, we should schedule an Update
772 + // effect even though we're bailing out, so that cWU/cDU are called.
773 + if (typeof instance.componentDidMount === 'function') {
774 + workInProgress.effectTag |= Update;
775 + }
776 + return false;
777 + }
778 +
779 + const shouldUpdate = checkShouldComponentUpdate(
780 + workInProgress,
781 + oldProps,
782 + newProps,
783 + oldState,
784 + newState,
785 + newContext,
786 + );
787 +
788 + if (shouldUpdate) {
789 + // In order to support react-lifecycles-compat polyfilled components,
790 + // Unsafe lifecycles should not be invoked for any component with the new gDSFP.
791 + if (
792 + (typeof instance.UNSAFE_componentWillUpdate === 'function' ||
793 + typeof instance.componentWillUpdate === 'function') &&
794 + typeof ctor.getDerivedStateFromProps !== 'function'
795 + ) {
796 + startPhaseTimer(workInProgress, 'componentWillUpdate');
797 + if (typeof instance.componentWillUpdate === 'function') {
798 + instance.componentWillUpdate(newProps, newState, newContext);
799 + }
800 + if (typeof instance.UNSAFE_componentWillUpdate === 'function') {
801 + instance.UNSAFE_componentWillUpdate(newProps, newState, newContext);
802 + }
803 + stopPhaseTimer();
804 + }
805 + if (typeof instance.componentDidUpdate === 'function') {
806 + workInProgress.effectTag |= Update;
807 + }
808 + } else {
809 + // If an update was already in progress, we should schedule an Update
810 + // effect even though we're bailing out, so that cWU/cDU are called.
811 + if (typeof instance.componentDidMount === 'function') {
812 + workInProgress.effectTag |= Update;
813 + }
814 +
815 + // If shouldComponentUpdate returned false, we should still update the
816 + // memoized props/state to indicate that this work can be reused.
817 + memoizeProps(workInProgress, newProps);
818 + memoizeState(workInProgress, newState);
819 + }
820 +
821 + // Update the existing instance's state, props, and context pointers even
822 + // if shouldComponentUpdate returns false.
823 + instance.props = newProps;
824 + instance.state = newState;
825 + instance.context = newContext;
826 +
827 + return shouldUpdate;
828 + }
829
830 // Invokes the update life-cycles and returns false if it shouldn't rerender.
831 function updateClassInstance(
@@ -756,6 +833,7 @@ export default function(
833 workInProgress: Fiber,
834 renderExpirationTime: ExpirationTime,
835 ): boolean {
836 + const ctor = workInProgress.type;
837 const instance = workInProgress.stateNode;
838 resetInputPointers(workInProgress, instance);
839
@@ -774,7 +852,7 @@ export default function(
852 if (
853 (typeof instance.UNSAFE_componentWillReceiveProps === 'function' ||
854 typeof instance.componentWillReceiveProps === 'function') &&
777 - typeof workInProgress.type.getDerivedStateFromProps !== 'function'
855 + typeof ctor.getDerivedStateFromProps !== 'function'
856 ) {
857 if (oldProps !== newProps || oldContext !== newContext) {
858 callComponentWillReceiveProps(
@@ -786,9 +864,9 @@ export default function(
864 }
865 }
866
789 - let partialState;
867 + let derivedStateFromProps;
868 if (oldProps !== newProps) {
791 - partialState = callGetDerivedStateFromProps(
869 + derivedStateFromProps = callGetDerivedStateFromProps(
870 workInProgress,
871 instance,
872 newProps,
@@ -799,6 +877,7 @@ export default function(
877 const oldState = workInProgress.memoizedState;
878 // TODO: Previous state can be null.
879 let newState;
880 + let derivedStateFromCatch;
881 if (workInProgress.updateQueue !== null) {
882 newState = processUpdateQueue(
883 current,
@@ -808,18 +887,45 @@ export default function(
887 newProps,
888 renderExpirationTime,
889 );
890 +
891 + let updateQueue = workInProgress.updateQueue;
892 + if (
893 + updateQueue !== null &&
894 + updateQueue.capturedValues !== null &&
895 + (enableGetDerivedStateFromCatch &&
896 + typeof ctor.getDerivedStateFromCatch === 'function')
897 + ) {
898 + const capturedValues = updateQueue.capturedValues;
899 + // Don't remove these from the update queue yet. We need them in
900 + // finishClassComponent. Do the reset there.
901 + // TODO: This is awkward. Refactor class components.
902 + // updateQueue.capturedValues = null;
903 + derivedStateFromCatch = callGetDerivedStateFromCatch(
904 + ctor,
905 + capturedValues,
906 + );
907 + }
908 } else {
909 newState = oldState;
910 }
911
815 - if (partialState !== null && partialState !== undefined) {
912 + if (derivedStateFromProps !== null && derivedStateFromProps !== undefined) {
913 + // Render-phase updates (like this) should not be added to the update queue,
914 + // So that multiple render passes do not enqueue multiple updates.
915 + // Instead, just synchronously merge the returned state into the instance.
916 + newState =
917 + newState === null || newState === undefined
918 + ? derivedStateFromProps
919 + : Object.assign({}, newState, derivedStateFromProps);
920 + }
921 + if (derivedStateFromCatch !== null && derivedStateFromCatch !== undefined) {
922 // Render-phase updates (like this) should not be added to the update queue,
923 // So that multiple render passes do not enqueue multiple updates.
924 // Instead, just synchronously merge the returned state into the instance.
925 newState =
926 newState === null || newState === undefined
821 - ? partialState
822 - : Object.assign({}, newState, partialState);
927 + ? derivedStateFromCatch
928 + : Object.assign({}, newState, derivedStateFromCatch);
929 }
930
931 if (
@@ -859,7 +965,7 @@ export default function(
965 if (
966 (typeof instance.UNSAFE_componentWillUpdate === 'function' ||
967 typeof instance.componentWillUpdate === 'function') &&
862 - typeof workInProgress.type.getDerivedStateFromProps !== 'function'
968 + typeof ctor.getDerivedStateFromProps !== 'function'
969 ) {
970 startPhaseTimer(workInProgress, 'componentWillUpdate');
971 if (typeof instance.componentWillUpdate === 'function') {
@@ -905,7 +1011,7 @@ export default function(
1011 callGetDerivedStateFromProps,
1012 constructClassInstance,
1013 mountClassInstance,
908 - // resumeMountClassInstance,
1014 + resumeMountClassInstance,
1015 updateClassInstance,
1016 };
1017 }
packages/react-reconciler/src/ReactFiberCommitWork.js
+128 -1
@@ -9,6 +9,9 @@
9
10 import type {HostConfig} from 'react-reconciler';
11 import type {Fiber} from './ReactFiber';
12 +import type {FiberRoot} from './ReactFiber';
13 +import type {ExpirationTime} from './ReactFiberExpirationTime';
14 +import type {CapturedValue, CapturedError} from './ReactCapturedValue';
15
16 import {
17 enableMutatingReconciler,
@@ -30,6 +33,9 @@ import invariant from 'fbjs/lib/invariant';
33 import {commitCallbacks} from './ReactFiberUpdateQueue';
34 import {onCommitUnmount} from './ReactFiberDevToolsHook';
35 import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf';
36 +import {logCapturedError} from './ReactFiberErrorLogger';
37 +import getComponentName from 'shared/getComponentName';
38 +import {getStackAddendumByWorkInProgressFiber} from 'shared/ReactFiberComponentTreeHook';
39
40 const {
41 invokeGuardedCallback,
@@ -37,9 +43,58 @@ const {
43 clearCaughtError,
44 } = ReactErrorUtils;
45
46 +function logError(boundary: Fiber, errorInfo: CapturedValue<mixed>) {
47 + const source = errorInfo.source;
48 + let stack = errorInfo.stack;
49 + if (stack === null) {
50 + stack = getStackAddendumByWorkInProgressFiber(source);
51 + }
52 +
53 + const capturedError: CapturedError = {
54 + componentName: source !== null ? getComponentName(source) : null,
55 + error: errorInfo.value,
56 + errorBoundary: boundary,
57 + componentStack: stack !== null ? stack : '',
58 + errorBoundaryName: null,
59 + errorBoundaryFound: false,
60 + willRetry: false,
61 + };
62 +
63 + if (boundary !== null) {
64 + capturedError.errorBoundaryName = getComponentName(boundary);
65 + capturedError.errorBoundaryFound = capturedError.willRetry =
66 + boundary.tag === ClassComponent;
67 + } else {
68 + capturedError.errorBoundaryName = null;
69 + capturedError.errorBoundaryFound = capturedError.willRetry = false;
70 + }
71 +
72 + try {
73 + logCapturedError(capturedError);
74 + } catch (e) {
75 + // Prevent cycle if logCapturedError() throws.
76 + // A cycle may still occur if logCapturedError renders a component that throws.
77 + const suppressLogging = e && e.suppressReactErrorLogging;
78 + if (!suppressLogging) {
79 + console.error(e);
80 + }
81 + }
82 +}
83 +
84 export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
85 config: HostConfig<T, P, I, TI, HI, PI, C, CC, CX, PL>,
86 captureError: (failedFiber: Fiber, error: mixed) => Fiber | null,
87 + scheduleWork: (
88 + fiber: Fiber,
89 + startTime: ExpirationTime,
90 + expirationTime: ExpirationTime,
91 + ) => void,
92 + computeExpirationForFiber: (
93 + startTime: ExpirationTime,
94 + fiber: Fiber,
95 + ) => ExpirationTime,
96 + markLegacyErrorBoundaryAsFailed: (instance: mixed) => void,
97 + recalculateCurrentTime: () => ExpirationTime,
98 ) {
99 const {getPublicInstance, mutation, persistence} = config;
100
@@ -97,7 +152,13 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
152 }
153 }
154
100 - function commitLifeCycles(current: Fiber | null, finishedWork: Fiber): void {
155 + function commitLifeCycles(
156 + finishedRoot: FiberRoot,
157 + current: Fiber | null,
158 + finishedWork: Fiber,
159 + currentTime: ExpirationTime,
160 + committedExpirationTime: ExpirationTime,
161 + ): void {
162 switch (finishedWork.tag) {
163 case ClassComponent: {
164 const instance = finishedWork.stateNode;
@@ -175,6 +236,70 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
236 }
237 }
238
239 + function commitErrorLogging(
240 + finishedWork: Fiber,
241 + onUncaughtError: (error: Error) => void,
242 + ) {
243 + switch (finishedWork.tag) {
244 + case ClassComponent:
245 + {
246 + const ctor = finishedWork.type;
247 + const instance = finishedWork.stateNode;
248 + const updateQueue = finishedWork.updateQueue;
249 + invariant(
250 + updateQueue !== null && updateQueue.capturedValues !== null,
251 + 'An error logging effect should not have been scheduled if no errors ' +
252 + 'were captured. This error is likely caused by a bug in React. ' +
253 + 'Please file an issue.',
254 + );
255 + const capturedErrors = updateQueue.capturedValues;
256 + updateQueue.capturedValues = null;
257 +
258 + if (typeof ctor.getDerivedStateFromCatch !== 'function') {
259 + // To preserve the preexisting retry behavior of error boundaries,
260 + // we keep track of which ones already failed during this batch.
261 + // This gets reset before we yield back to the browser.
262 + // TODO: Warn in strict mode if getDerivedStateFromCatch is
263 + // not defined.
264 + markLegacyErrorBoundaryAsFailed(instance);
265 + }
266 +
267 + instance.props = finishedWork.memoizedProps;
268 + instance.state = finishedWork.memoizedState;
269 + for (let i = 0; i < capturedErrors.length; i++) {
270 + const errorInfo = capturedErrors[i];
271 + const error = errorInfo.value;
272 + logError(finishedWork, errorInfo);
273 + instance.componentDidCatch(error);
274 + }
275 + }
276 + break;
277 + case HostRoot: {
278 + const updateQueue = finishedWork.updateQueue;
279 + invariant(
280 + updateQueue !== null && updateQueue.capturedValues !== null,
281 + 'An error logging effect should not have been scheduled if no errors ' +
282 + 'were captured. This error is likely caused by a bug in React. ' +
283 + 'Please file an issue.',
284 + );
285 + const capturedErrors = updateQueue.capturedValues;
286 + updateQueue.capturedValues = null;
287 + for (let i = 0; i < capturedErrors.length; i++) {
288 + const errorInfo = capturedErrors[i];
289 + logError(finishedWork, errorInfo);
290 + onUncaughtError(errorInfo.value);
291 + }
292 + break;
293 + }
294 + default:
295 + invariant(
296 + false,
297 + 'This unit of work tag cannot capture errors. This error is ' +
298 + 'likely caused by a bug in React. Please file an issue.',
299 + );
300 + }
301 + }
302 +
303 function commitAttachRef(finishedWork: Fiber) {
304 const ref = finishedWork.ref;
305 if (ref !== null) {
@@ -353,6 +478,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
478 commitContainer(finishedWork);
479 },
480 commitLifeCycles,
481 + commitErrorLogging,
482 commitAttachRef,
483 commitDetachRef,
484 };
@@ -679,6 +805,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
805 commitDeletion,
806 commitWork,
807 commitLifeCycles,
808 + commitErrorLogging,
809 commitAttachRef,
810 commitDetachRef,
811 };
packages/react-reconciler/src/ReactFiberCompleteWork.js
+29 -2
@@ -35,7 +35,13 @@ import {
35 Fragment,
36 Mode,
37 } from 'shared/ReactTypeOfWork';
38 -import {Placement, Ref, Update} from 'shared/ReactTypeOfSideEffect';
38 +import {
39 + Placement,
40 + Ref,
41 + Update,
42 + ErrLog,
43 + DidCapture,
44 +} from 'shared/ReactTypeOfSideEffect';
45 import invariant from 'fbjs/lib/invariant';
46
47 import {reconcileChildFibers} from './ReactChildFiber';
@@ -405,6 +411,20 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
411 case ClassComponent: {
412 // We are leaving this subtree, so pop context if any.
413 popLegacyContextProvider(workInProgress);
414 +
415 + // If this component caught an error, schedule an error log effect.
416 + const instance = workInProgress.stateNode;
417 + const updateQueue = workInProgress.updateQueue;
418 + if (updateQueue !== null && updateQueue.capturedValues !== null) {
419 + workInProgress.effectTag &= ~DidCapture;
420 + if (typeof instance.componentDidCatch === 'function') {
421 + workInProgress.effectTag |= ErrLog;
422 + } else {
423 + // Normally we clear this in the commit phase, but since we did not
424 + // schedule an effect, we need to reset it here.
425 + updateQueue.capturedValues = null;
426 + }
427 + }
428 return null;
429 }
430 case HostRoot: {
@@ -415,7 +435,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
435 fiberRoot.context = fiberRoot.pendingContext;
436 fiberRoot.pendingContext = null;
437 }
418 -
438 if (current === null || current.child === null) {
439 // If we hydrated, pop so that we can delete any remaining children
440 // that weren't hydrated.
@@ -425,6 +444,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
444 workInProgress.effectTag &= ~Placement;
445 }
446 updateHostContainer(workInProgress);
447 +
448 + const updateQueue = workInProgress.updateQueue;
449 + if (updateQueue !== null && updateQueue.capturedValues !== null) {
450 + workInProgress.effectTag |= ErrLog;
451 + }
452 return null;
453 }
454 case HostComponent: {
@@ -441,6 +465,9 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
465 // Even better would be if children weren't special cased at all tho.
466 const instance: I = workInProgress.stateNode;
467 const currentHostContext = getHostContext();
468 + // TODO: Experiencing an error where oldProps is null. Suggests a host
469 + // component is hitting the resume path. Figure out why. Possibly
470 + // related to `hidden`.
471 const updatePayload = prepareUpdate(
472 instance,
473 type,
packages/react-reconciler/src/ReactFiberErrorDialog.js
+1 -1
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import type {CapturedError} from './ReactFiberScheduler';
10 +import type {CapturedError} from './ReactCapturedValue';
11
12 // This module is forked in different environments.
13 // By default, return `true` to log errors to the console.
packages/react-reconciler/src/ReactFiberErrorLogger.js
+1 -1
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import type {CapturedError} from './ReactFiberScheduler';
10 +import type {CapturedError} from './ReactCapturedValue';
11
12 import {showErrorDialog} from './ReactFiberErrorDialog';
13
packages/react-reconciler/src/ReactFiberReconciler.js
+30 -2
@@ -295,6 +295,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
295
296 const {
297 computeUniqueAsyncExpiration,
298 + recalculateCurrentTime,
299 computeExpirationForFiber,
300 scheduleWork,
301 requestWork,
@@ -312,6 +313,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
313 function scheduleRootUpdate(
314 current: Fiber,
315 element: ReactNodeList,
316 + currentTime: ExpirationTime,
317 expirationTime: ExpirationTime,
318 callback: ?Function,
319 ) {
@@ -349,6 +351,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
351 callback,
352 isReplace: false,
353 isForced: false,
354 + capturedValue: null,
355 next: null,
356 };
357 insertUpdateIntoFiber(current, update);
@@ -361,6 +364,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
364 element: ReactNodeList,
365 container: OpaqueRoot,
366 parentComponent: ?React$Component<any, any>,
367 + currentTime: ExpirationTime,
368 expirationTime: ExpirationTime,
369 callback: ?Function,
370 ) {
@@ -386,7 +390,13 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
390 container.pendingContext = context;
391 }
392
389 - return scheduleRootUpdate(current, element, expirationTime, callback);
393 + return scheduleRootUpdate(
394 + current,
395 + element,
396 + currentTime,
397 + expirationTime,
398 + callback,
399 + );
400 }
401
402 function findHostInstance(fiber: Fiber): PI | null {
@@ -413,17 +423,35 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
423 callback: ?Function,
424 ): ExpirationTime {
425 const current = container.current;
426 + const currentTime = recalculateCurrentTime();
427 const expirationTime = computeExpirationForFiber(current);
428 return updateContainerAtExpirationTime(
429 element,
430 container,
431 parentComponent,
432 + currentTime,
433 expirationTime,
434 callback,
435 );
436 },
437
426 - updateContainerAtExpirationTime,
438 + updateContainerAtExpirationTime(
439 + element,
440 + container,
441 + parentComponent,
442 + expirationTime,
443 + callback,
444 + ) {
445 + const currentTime = recalculateCurrentTime();
446 + return updateContainerAtExpirationTime(
447 + element,
448 + container,
449 + parentComponent,
450 + currentTime,
451 + expirationTime,
452 + callback,
453 + );
454 + },
455
456 flushRoot,
457
packages/react-reconciler/src/ReactFiberRoot.js
+6 -6
@@ -28,10 +28,7 @@ export type FiberRoot = {
28 pendingChildren: any,
29 // The currently active root fiber. This is the mutable root of the tree.
30 current: Fiber,
31 - // Remaining expiration time on this root.
32 - remainingExpirationTime: ExpirationTime,
33 - // Determines if this root can be committed.
34 - isReadyForCommit: boolean,
31 + pendingCommitExpirationTime: ExpirationTime,
32 // A finished work-in-progress HostRoot that's ready to be committed.
33 // TODO: The reason this is separate from isReadyForCommit is because the
34 // FiberRoot concept will likely be lifted out of the reconciler and into
@@ -42,6 +39,9 @@ export type FiberRoot = {
39 pendingContext: Object | null,
40 // Determines if we should attempt to hydrate on the initial mount
41 +hydrate: boolean,
42 + // Remaining expiration time on this root.
43 + // TODO: Lift this into the renderer
44 + remainingExpirationTime: ExpirationTime,
45 // List of top-level batches. This list indicates whether a commit should be
46 // deferred. Also contains completion callbacks.
47 // TODO: Lift this into the renderer
@@ -62,12 +62,12 @@ export function createFiberRoot(
62 current: uninitializedFiber,
63 containerInfo: containerInfo,
64 pendingChildren: null,
65 - remainingExpirationTime: NoWork,
66 - isReadyForCommit: false,
65 + pendingCommitExpirationTime: NoWork,
66 finishedWork: null,
67 context: null,
68 pendingContext: null,
69 hydrate,
70 + remainingExpirationTime: NoWork,
71 firstBatch: null,
72 nextScheduledRoot: null,
73 };
packages/react-reconciler/src/ReactFiberScheduler.js
+432 -623
@@ -13,11 +13,11 @@ import type {FiberRoot, Batch} from './ReactFiberRoot';
13 import type {HydrationContext} from './ReactFiberHydrationContext';
14 import type {ExpirationTime} from './ReactFiberExpirationTime';
15
16 -import {getStackAddendumByWorkInProgressFiber} from 'shared/ReactFiberComponentTreeHook';
16 import ReactErrorUtils from 'shared/ReactErrorUtils';
17 import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState';
18 import ReactStrictModeWarnings from './ReactStrictModeWarnings';
19 import {
20 + NoEffect,
21 PerformedWork,
22 Placement,
23 Update,
@@ -25,19 +25,23 @@ import {
25 Deletion,
26 ContentReset,
27 Callback,
28 - Err,
28 + DidCapture,
29 Ref,
30 + Incomplete,
31 + HostEffectMask,
32 + ErrLog,
33 } from 'shared/ReactTypeOfSideEffect';
34 import {
35 HostRoot,
33 - HostComponent,
34 - HostPortal,
36 ClassComponent,
37 + HostComponent,
38 ContextProvider,
39 + HostPortal,
40 } from 'shared/ReactTypeOfWork';
41 import {
42 enableUserTimingAPI,
43 warnAboutDeprecatedLifecycles,
44 + replayFailedUnitOfWorkWithInvokeGuardedCallback,
45 } from 'shared/ReactFeatureFlags';
46 import getComponentName from 'shared/getComponentName';
47 import invariant from 'fbjs/lib/invariant';
@@ -45,6 +49,7 @@ import warning from 'fbjs/lib/warning';
49
50 import ReactFiberBeginWork from './ReactFiberBeginWork';
51 import ReactFiberCompleteWork from './ReactFiberCompleteWork';
52 +import ReactFiberUnwindWork from './ReactFiberUnwindWork';
53 import ReactFiberCommitWork from './ReactFiberCommitWork';
54 import ReactFiberHostContext from './ReactFiberHostContext';
55 import ReactFiberHydrationContext from './ReactFiberHydrationContext';
@@ -67,10 +72,7 @@ import {
72 startCommitLifeCyclesTimer,
73 stopCommitLifeCyclesTimer,
74 } from './ReactDebugFiberPerf';
70 -import {popContextProvider} from './ReactFiberContext';
71 -import {popProvider} from './ReactFiberNewContext';
75 import {reset} from './ReactFiberStack';
73 -import {logCapturedError} from './ReactFiberErrorLogger';
76 import {createWorkInProgress} from './ReactFiber';
77 import {onCommitRoot} from './ReactFiberDevToolsHook';
78 import {
@@ -82,9 +84,18 @@ import {
84 computeExpirationBucket,
85 } from './ReactFiberExpirationTime';
86 import {AsyncMode} from './ReactTypeOfMode';
85 -import {getUpdateExpirationTime} from './ReactFiberUpdateQueue';
86 -import {resetContext as resetLegacyContext} from './ReactFiberContext';
87 +import {
88 + resetContext as resetLegacyContext,
89 + popContextProvider as popLegacyContextProvider,
90 + popTopLevelContextObject as popTopLevelLegacyContextObject,
91 +} from './ReactFiberContext';
92 +import {popProvider} from './ReactFiberNewContext';
93 import {resetProviderStack} from './ReactFiberNewContext';
94 +import {
95 + getUpdateExpirationTime,
96 + insertUpdateIntoFiber,
97 +} from './ReactFiberUpdateQueue';
98 +import {createCapturedValue} from './ReactCapturedValue';
99
100 const {
101 invokeGuardedCallback,
@@ -92,20 +103,6 @@ const {
103 clearCaughtError,
104 } = ReactErrorUtils;
105
95 -export type CapturedError = {
96 - componentName: ?string,
97 - componentStack: string,
98 - error: mixed,
99 - errorBoundary: ?Object,
100 - errorBoundaryFound: boolean,
101 - errorBoundaryName: string | null,
102 - willRetry: boolean,
103 -};
104 -
105 -export type HandleErrorInfo = {
106 - componentStack: string,
107 -};
108 -
106 let didWarnAboutStateTransition;
107 let didWarnSetStateChildContext;
108 let warnAboutUpdateOnUnmounted;
@@ -165,11 +162,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
162 config: HostConfig<T, P, I, TI, HI, PI, C, CC, CX, PL>,
163 ) {
164 const hostContext = ReactFiberHostContext(config);
165 + const {popHostContext, popHostContainer} = hostContext;
166 const hydrationContext: HydrationContext<C, CX> = ReactFiberHydrationContext(
167 config,
168 );
171 - const {popHostContainer, popHostContext, resetHostContainer} = hostContext;
172 - const {beginWork, beginFailedWork} = ReactFiberBeginWork(
169 + const {resetHostContainer} = hostContext;
170 + const {beginWork} = ReactFiberBeginWork(
171 config,
172 hostContext,
173 hydrationContext,
@@ -181,15 +179,28 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
179 hostContext,
180 hydrationContext,
181 );
182 + const {throwException, unwindWork} = ReactFiberUnwindWork(
183 + hostContext,
184 + scheduleWork,
185 + isAlreadyFailedLegacyErrorBoundary,
186 + );
187 const {
188 commitResetTextContent,
189 commitPlacement,
190 commitDeletion,
191 commitWork,
192 commitLifeCycles,
193 + commitErrorLogging,
194 commitAttachRef,
195 commitDetachRef,
192 - } = ReactFiberCommitWork(config, captureError);
196 + } = ReactFiberCommitWork(
197 + config,
198 + onCommitPhaseError,
199 + scheduleWork,
200 + computeExpirationForFiber,
201 + markLegacyErrorBoundaryAsFailed,
202 + recalculateCurrentTime,
203 + );
204 const {
205 now,
206 scheduleDeferredCallback,
@@ -199,8 +210,9 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
210 } = config;
211
212 // Represents the current time in ms.
202 - const startTime = now();
213 + const originalStartTimeMs = now();
214 let mostRecentCurrentTime: ExpirationTime = msToExpirationTime(0);
215 + let mostRecentCurrentTimeMs: number = originalStartTimeMs;
216
217 // Used to ensure computeUniqueAsyncExpiration is monotonically increases.
218 let lastUniqueAsyncExpiration: number = 0;
@@ -221,32 +233,70 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
233 // The next fiber with an effect that we're currently committing.
234 let nextEffect: Fiber | null = null;
235
224 - // Keep track of which fibers have captured an error that need to be handled.
225 - // Work is removed from this collection after componentDidCatch is called.
226 - let capturedErrors: Map<Fiber, CapturedError> | null = null;
227 - // Keep track of which fibers have failed during the current batch of work.
228 - // This is a different set than capturedErrors, because it is not reset until
229 - // the end of the batch. This is needed to propagate errors correctly if a
230 - // subtree fails more than once.
231 - let failedBoundaries: Set<Fiber> | null = null;
232 - // Error boundaries that captured an error during the current commit.
233 - let commitPhaseBoundaries: Set<Fiber> | null = null;
234 - let firstUncaughtError: mixed | null = null;
235 - let didFatal: boolean = false;
236 -
236 let isCommitting: boolean = false;
238 - let isUnmounting: boolean = false;
237 +
238 + let isRootReadyForCommit: boolean = false;
239 +
240 + let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
241
242 // Used for performance tracking.
243 let interruptedBy: Fiber | null = null;
244
245 + let stashedWorkInProgressProperties;
246 + let replayUnitOfWork;
247 + if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
248 + stashedWorkInProgressProperties = null;
249 + replayUnitOfWork = (failedUnitOfWork: Fiber, isAsync: boolean) => {
250 + // Retore the original state of the work-in-progress
251 + Object.assign(failedUnitOfWork, stashedWorkInProgressProperties);
252 + switch (failedUnitOfWork.tag) {
253 + case HostRoot:
254 + popHostContainer(failedUnitOfWork);
255 + popTopLevelLegacyContextObject(failedUnitOfWork);
256 + break;
257 + case HostComponent:
258 + popHostContext(failedUnitOfWork);
259 + break;
260 + case ClassComponent:
261 + popLegacyContextProvider(failedUnitOfWork);
262 + break;
263 + case HostPortal:
264 + popHostContainer(failedUnitOfWork);
265 + break;
266 + case ContextProvider:
267 + popProvider(failedUnitOfWork);
268 + break;
269 + }
270 + // Replay the begin phase.
271 + invokeGuardedCallback(null, workLoop, null, isAsync);
272 + if (hasCaughtError()) {
273 + clearCaughtError();
274 + } else {
275 + // This should be unreachable because the render phase is
276 + // idempotent
277 + }
278 + };
279 + }
280 +
281 function resetContextStack() {
282 // Reset the stack
283 reset();
284 // Reset the cursors
285 resetLegacyContext();
248 - resetProviderStack();
286 resetHostContainer();
287 +
288 + // TODO: Unify new context implementation with other stacks
289 + resetProviderStack();
290 +
291 + if (__DEV__) {
292 + ReactStrictModeWarnings.discardPendingWarnings();
293 + }
294 +
295 + nextRoot = null;
296 + nextRenderExpirationTime = NoWork;
297 + nextUnitOfWork = null;
298 +
299 + isRootReadyForCommit = false;
300 }
301
302 function commitAllHostEffects() {
@@ -272,8 +322,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
322 // updates, and deletions. To avoid needing to add a case for every
323 // possible bitmap value, we remove the secondary effects from the
324 // effect tag and switch on that value.
275 - let primaryEffectTag =
276 - effectTag & ~(Callback | Err | ContentReset | Ref | PerformedWork);
325 + let primaryEffectTag = effectTag & (Placement | Update | Deletion);
326 switch (primaryEffectTag) {
327 case Placement: {
328 commitPlacement(nextEffect);
@@ -303,9 +352,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
352 break;
353 }
354 case Deletion: {
306 - isUnmounting = true;
355 commitDeletion(nextEffect);
308 - isUnmounting = false;
356 break;
357 }
358 }
@@ -317,7 +364,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
364 }
365 }
366
320 - function commitAllLifeCycles() {
367 + function commitAllLifeCycles(
368 + finishedRoot: FiberRoot,
369 + currentTime: ExpirationTime,
370 + committedExpirationTime: ExpirationTime,
371 + ) {
372 if (__DEV__) {
373 ReactStrictModeWarnings.flushPendingUnsafeLifecycleWarnings();
374
@@ -325,24 +376,28 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
376 ReactStrictModeWarnings.flushPendingDeprecationWarnings();
377 }
378 }
328 -
379 while (nextEffect !== null) {
380 const effectTag = nextEffect.effectTag;
381
382 if (effectTag & (Update | Callback)) {
383 recordEffect();
384 const current = nextEffect.alternate;
335 - commitLifeCycles(current, nextEffect);
385 + commitLifeCycles(
386 + finishedRoot,
387 + current,
388 + nextEffect,
389 + currentTime,
390 + committedExpirationTime,
391 + );
392 }
393
338 - if (effectTag & Ref) {
339 - recordEffect();
340 - commitAttachRef(nextEffect);
394 + if (effectTag & ErrLog) {
395 + commitErrorLogging(nextEffect, onUncaughtError);
396 }
397
343 - if (effectTag & Err) {
398 + if (effectTag & Ref) {
399 recordEffect();
345 - commitErrorHandling(nextEffect);
400 + commitAttachRef(nextEffect);
401 }
402
403 const next = nextEffect.nextEffect;
@@ -357,11 +412,22 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
412 }
413 }
414
415 + function isAlreadyFailedLegacyErrorBoundary(instance: mixed): boolean {
416 + return (
417 + legacyErrorBoundariesThatAlreadyFailed !== null &&
418 + legacyErrorBoundariesThatAlreadyFailed.has(instance)
419 + );
420 + }
421 +
422 + function markLegacyErrorBoundaryAsFailed(instance: mixed) {
423 + if (legacyErrorBoundariesThatAlreadyFailed === null) {
424 + legacyErrorBoundariesThatAlreadyFailed = new Set([instance]);
425 + } else {
426 + legacyErrorBoundariesThatAlreadyFailed.add(instance);
427 + }
428 + }
429 +
430 function commitRoot(finishedWork: Fiber): ExpirationTime {
361 - // We keep track of this so that captureError can collect any boundaries
362 - // that capture an error during the commit phase. The reason these aren't
363 - // local to this function is because errors that occur during cWU are
364 - // captured elsewhere, to prevent the unmount from being interrupted.
431 isWorking = true;
432 isCommitting = true;
433 startCommitTimer();
@@ -373,7 +439,15 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
439 'related to the return field. This error is likely caused by a bug ' +
440 'in React. Please file an issue.',
441 );
376 - root.isReadyForCommit = false;
442 + const committedExpirationTime = root.pendingCommitExpirationTime;
443 + invariant(
444 + committedExpirationTime !== NoWork,
445 + 'Cannot commit an incomplete root. This error is likely caused by a ' +
446 + 'bug in React. Please file an issue.',
447 + );
448 + root.pendingCommitExpirationTime = NoWork;
449 +
450 + const currentTime = recalculateCurrentTime();
451
452 // Reset this to null before calling lifecycles
453 ReactCurrentOwner.current = null;
@@ -425,7 +499,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
499 'Should have next effect. This error is likely caused by a bug ' +
500 'in React. Please file an issue.',
501 );
428 - captureError(nextEffect, error);
502 + onCommitPhaseError(nextEffect, error);
503 // Clean-up
504 if (nextEffect !== null) {
505 nextEffect = nextEffect.nextEffect;
@@ -452,14 +526,21 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
526 let didError = false;
527 let error;
528 if (__DEV__) {
455 - invokeGuardedCallback(null, commitAllLifeCycles, null);
529 + invokeGuardedCallback(
530 + null,
531 + commitAllLifeCycles,
532 + null,
533 + root,
534 + currentTime,
535 + committedExpirationTime,
536 + );
537 if (hasCaughtError()) {
538 didError = true;
539 error = clearCaughtError();
540 }
541 } else {
542 try {
462 - commitAllLifeCycles();
543 + commitAllLifeCycles(root, currentTime, committedExpirationTime);
544 } catch (e) {
545 didError = true;
546 error = e;
@@ -471,7 +552,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
552 'Should have next effect. This error is likely caused by a bug ' +
553 'in React. Please file an issue.',
554 );
474 - captureError(nextEffect, error);
555 + onCommitPhaseError(nextEffect, error);
556 if (nextEffect !== null) {
557 nextEffect = nextEffect.nextEffect;
558 }
@@ -489,26 +570,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
570 ReactFiberInstrumentation.debugTool.onCommitWork(finishedWork);
571 }
572
492 - // If we caught any errors during this commit, schedule their boundaries
493 - // to update.
494 - if (commitPhaseBoundaries) {
495 - commitPhaseBoundaries.forEach(scheduleErrorRecovery);
496 - commitPhaseBoundaries = null;
497 - }
498 -
499 - if (firstUncaughtError !== null) {
500 - const error = firstUncaughtError;
501 - firstUncaughtError = null;
502 - onUncaughtError(error);
503 - }
504 -
573 const remainingTime = root.current.expirationTime;
506 -
574 if (remainingTime === NoWork) {
508 - capturedErrors = null;
509 - failedBoundaries = null;
575 + // If there's no remaining work, we can clear the set of already failed
576 + // error boundaries.
577 + legacyErrorBoundariesThatAlreadyFailed = null;
578 }
511 -
579 return remainingTime;
580 }
581
@@ -543,6 +610,9 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
610 }
611
612 function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
613 + // Attempt to complete the current unit of work, then move to the
614 + // next sibling. If there are no more siblings, return to the
615 + // parent fiber.
616 while (true) {
617 // The current, flushed, state of this fiber is the alternate.
618 // Ideally nothing should rely on this, but relying on it here
@@ -552,80 +622,136 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
622 if (__DEV__) {
623 ReactDebugCurrentFiber.setCurrentFiber(workInProgress);
624 }
555 - const next = completeWork(
556 - current,
557 - workInProgress,
558 - nextRenderExpirationTime,
559 - );
560 - if (__DEV__) {
561 - ReactDebugCurrentFiber.resetCurrentFiber();
562 - }
625
626 const returnFiber = workInProgress.return;
627 const siblingFiber = workInProgress.sibling;
628
567 - resetExpirationTime(workInProgress, nextRenderExpirationTime);
568 -
569 - if (next !== null) {
629 + if ((workInProgress.effectTag & Incomplete) === NoEffect) {
630 + // This fiber completed.
631 + let next = completeWork(
632 + current,
633 + workInProgress,
634 + nextRenderExpirationTime,
635 + );
636 stopWorkTimer(workInProgress);
637 + resetExpirationTime(workInProgress, nextRenderExpirationTime);
638 + if (__DEV__) {
639 + ReactDebugCurrentFiber.resetCurrentFiber();
640 + }
641 +
642 + if (next !== null) {
643 + stopWorkTimer(workInProgress);
644 + if (__DEV__ && ReactFiberInstrumentation.debugTool) {
645 + ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress);
646 + }
647 + // If completing this work spawned new work, do that next. We'll come
648 + // back here again.
649 + return next;
650 + }
651 +
652 + if (
653 + returnFiber !== null &&
654 + // Do not append effects to parents if a sibling failed to complete
655 + (returnFiber.effectTag & Incomplete) === NoEffect
656 + ) {
657 + // Append all the effects of the subtree and this fiber onto the effect
658 + // list of the parent. The completion order of the children affects the
659 + // side-effect order.
660 + if (returnFiber.firstEffect === null) {
661 + returnFiber.firstEffect = workInProgress.firstEffect;
662 + }
663 + if (workInProgress.lastEffect !== null) {
664 + if (returnFiber.lastEffect !== null) {
665 + returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
666 + }
667 + returnFiber.lastEffect = workInProgress.lastEffect;
668 + }
669 +
670 + // If this fiber had side-effects, we append it AFTER the children's
671 + // side-effects. We can perform certain side-effects earlier if
672 + // needed, by doing multiple passes over the effect list. We don't want
673 + // to schedule our own side-effect on our own list because if end up
674 + // reusing children we'll schedule this effect onto itself since we're
675 + // at the end.
676 + const effectTag = workInProgress.effectTag;
677 + // Skip both NoWork and PerformedWork tags when creating the effect list.
678 + // PerformedWork effect is read by React DevTools but shouldn't be committed.
679 + if (effectTag > PerformedWork) {
680 + if (returnFiber.lastEffect !== null) {
681 + returnFiber.lastEffect.nextEffect = workInProgress;
682 + } else {
683 + returnFiber.firstEffect = workInProgress;
684 + }
685 + returnFiber.lastEffect = workInProgress;
686 + }
687 + }
688 +
689 if (__DEV__ && ReactFiberInstrumentation.debugTool) {
690 ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress);
691 }
574 - // If completing this work spawned new work, do that next. We'll come
575 - // back here again.
576 - return next;
577 - }
692
579 - if (returnFiber !== null) {
580 - // Append all the effects of the subtree and this fiber onto the effect
581 - // list of the parent. The completion order of the children affects the
582 - // side-effect order.
583 - if (returnFiber.firstEffect === null) {
584 - returnFiber.firstEffect = workInProgress.firstEffect;
693 + if (siblingFiber !== null) {
694 + // If there is more work to do in this returnFiber, do that next.
695 + return siblingFiber;
696 + } else if (returnFiber !== null) {
697 + // If there's no more work in this returnFiber. Complete the returnFiber.
698 + workInProgress = returnFiber;
699 + continue;
700 + } else {
701 + // We've reached the root.
702 + isRootReadyForCommit = true;
703 + return null;
704 }
586 - if (workInProgress.lastEffect !== null) {
587 - if (returnFiber.lastEffect !== null) {
588 - returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
589 - }
590 - returnFiber.lastEffect = workInProgress.lastEffect;
705 + } else {
706 + // This fiber did not complete because something threw. Pop values off
707 + // the stack without entering the complete phase. If this is a boundary,
708 + // capture values if possible.
709 + const next = unwindWork(workInProgress);
710 + // Because this fiber did not complete, don't reset its expiration time.
711 + if (workInProgress.effectTag & DidCapture) {
712 + // Restarting an error boundary
713 + stopFailedWorkTimer(workInProgress);
714 + } else {
715 + stopWorkTimer(workInProgress);
716 }
717
593 - // If this fiber had side-effects, we append it AFTER the children's
594 - // side-effects. We can perform certain side-effects earlier if
595 - // needed, by doing multiple passes over the effect list. We don't want
596 - // to schedule our own side-effect on our own list because if end up
597 - // reusing children we'll schedule this effect onto itself since we're
598 - // at the end.
599 - const effectTag = workInProgress.effectTag;
600 - // Skip both NoWork and PerformedWork tags when creating the effect list.
601 - // PerformedWork effect is read by React DevTools but shouldn't be committed.
602 - if (effectTag > PerformedWork) {
603 - if (returnFiber.lastEffect !== null) {
604 - returnFiber.lastEffect.nextEffect = workInProgress;
605 - } else {
606 - returnFiber.firstEffect = workInProgress;
718 + if (__DEV__) {
719 + ReactDebugCurrentFiber.resetCurrentFiber();
720 + }
721 +
722 + if (next !== null) {
723 + stopWorkTimer(workInProgress);
724 + if (__DEV__ && ReactFiberInstrumentation.debugTool) {
725 + ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress);
726 }
608 - returnFiber.lastEffect = workInProgress;
727 + // If completing this work spawned new work, do that next. We'll come
728 + // back here again.
729 + // Since we're restarting, remove anything that is not a host effect
730 + // from the effect tag.
731 + next.effectTag &= HostEffectMask;
732 + return next;
733 }
610 - }
734
612 - stopWorkTimer(workInProgress);
613 - if (__DEV__ && ReactFiberInstrumentation.debugTool) {
614 - ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress);
615 - }
735 + if (returnFiber !== null) {
736 + // Mark the parent fiber as incomplete and clear its effect list.
737 + returnFiber.firstEffect = returnFiber.lastEffect = null;
738 + returnFiber.effectTag |= Incomplete;
739 + }
740
617 - if (siblingFiber !== null) {
618 - // If there is more work to do in this returnFiber, do that next.
619 - return siblingFiber;
620 - } else if (returnFiber !== null) {
621 - // If there's no more work in this returnFiber. Complete the returnFiber.
622 - workInProgress = returnFiber;
623 - continue;
624 - } else {
625 - // We've reached the root.
626 - const root: FiberRoot = workInProgress.stateNode;
627 - root.isReadyForCommit = true;
628 - return null;
741 + if (__DEV__ && ReactFiberInstrumentation.debugTool) {
742 + ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress);
743 + }
744 +
745 + if (siblingFiber !== null) {
746 + // If there is more work to do in this returnFiber, do that next.
747 + return siblingFiber;
748 + } else if (returnFiber !== null) {
749 + // If there's no more work in this returnFiber. Complete the returnFiber.
750 + workInProgress = returnFiber;
751 + continue;
752 + } else {
753 + return null;
754 + }
755 }
756 }
757
@@ -648,45 +774,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
774 ReactDebugCurrentFiber.setCurrentFiber(workInProgress);
775 }
776
651 - let next = beginWork(current, workInProgress, nextRenderExpirationTime);
652 - if (__DEV__) {
653 - ReactDebugCurrentFiber.resetCurrentFiber();
654 - }
655 - if (__DEV__ && ReactFiberInstrumentation.debugTool) {
656 - ReactFiberInstrumentation.debugTool.onBeginWork(workInProgress);
777 + if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
778 + stashedWorkInProgressProperties = Object.assign({}, workInProgress);
779 }
780 + let next = beginWork(current, workInProgress, nextRenderExpirationTime);
781
659 - if (next === null) {
660 - // If this doesn't spawn new work, complete the current work.
661 - next = completeUnitOfWork(workInProgress);
662 - }
663 -
664 - ReactCurrentOwner.current = null;
665 -
666 - return next;
667 - }
668 -
669 - function performFailedUnitOfWork(workInProgress: Fiber): Fiber | null {
670 - if (__DEV__) {
671 - ReactStrictModeWarnings.discardPendingWarnings();
672 - }
673 -
674 - // The current, flushed, state of this fiber is the alternate.
675 - // Ideally nothing should rely on this, but relying on it here
676 - // means that we don't need an additional field on the work in
677 - // progress.
678 - const current = workInProgress.alternate;
679 -
680 - // See if beginning this work spawns more work.
681 - startWorkTimer(workInProgress);
682 - if (__DEV__) {
683 - ReactDebugCurrentFiber.setCurrentFiber(workInProgress);
684 - }
685 - let next = beginFailedWork(
686 - current,
687 - workInProgress,
688 - nextRenderExpirationTime,
689 - );
782 if (__DEV__) {
783 ReactDebugCurrentFiber.resetCurrentFiber();
784 }
@@ -705,14 +797,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
797 }
798
799 function workLoop(isAsync) {
708 - if (capturedErrors !== null) {
709 - // If there are unhandled errors, switch to the slow work loop.
710 - // TODO: How to avoid this check in the fast path? Maybe the renderer
711 - // could keep track of which roots have unhandled errors and call a
712 - // forked version of renderRoot.
713 - slowWorkLoopThatChecksForFailedWork(isAsync);
714 - return;
715 - }
800 if (!isAsync) {
801 // Flush all expired work.
802 while (nextUnitOfWork !== null) {
@@ -726,52 +810,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
810 }
811 }
812
729 - function slowWorkLoopThatChecksForFailedWork(isAsync) {
730 - if (!isAsync) {
731 - // Flush all expired work.
732 - while (nextUnitOfWork !== null) {
733 - if (hasCapturedError(nextUnitOfWork)) {
734 - // Use a forked version of performUnitOfWork
735 - nextUnitOfWork = performFailedUnitOfWork(nextUnitOfWork);
736 - } else {
737 - nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
738 - }
739 - }
740 - } else {
741 - // Flush asynchronous work until the deadline runs out of time.
742 - while (nextUnitOfWork !== null && !shouldYield()) {
743 - if (hasCapturedError(nextUnitOfWork)) {
744 - // Use a forked version of performUnitOfWork
745 - nextUnitOfWork = performFailedUnitOfWork(nextUnitOfWork);
746 - } else {
747 - nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
748 - }
749 - }
750 - }
751 - }
752 -
753 - function renderRootCatchBlock(
754 - root: FiberRoot,
755 - failedWork: Fiber,
756 - boundary: Fiber,
757 - isAsync: boolean,
758 - ) {
759 - // We're going to restart the error boundary that captured the error.
760 - // Conceptually, we're unwinding the stack. We need to unwind the
761 - // context stack, too.
762 - unwindContexts(failedWork, boundary);
763 -
764 - // Restart the error boundary using a forked version of
765 - // performUnitOfWork that deletes the boundary's children. The entire
766 - // failed subree will be unmounted. During the commit phase, a special
767 - // lifecycle method is called on the error boundary, which triggers
768 - // a re-render.
769 - nextUnitOfWork = performFailedUnitOfWork(boundary);
770 -
771 - // Continue working.
772 - workLoop(isAsync);
773 - }
774 -
813 function renderRoot(
814 root: FiberRoot,
815 expirationTime: ExpirationTime,
@@ -784,15 +822,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
822 );
823 isWorking = true;
824
787 - // We're about to mutate the work-in-progress tree. If the root was pending
788 - // commit, it no longer is: we'll need to complete it again.
789 - root.isReadyForCommit = false;
790 -
825 // Check if we're starting from a fresh stack, or if we're resuming from
826 // previously yielded work.
827 if (
794 - root !== nextRoot ||
828 expirationTime !== nextRenderExpirationTime ||
829 + root !== nextRoot ||
830 nextUnitOfWork === null
831 ) {
832 // Reset the stack and start working from the root.
@@ -802,363 +836,149 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
836 nextUnitOfWork = createWorkInProgress(
837 nextRoot.current,
838 null,
805 - expirationTime,
839 + nextRenderExpirationTime,
840 );
841 + root.pendingCommitExpirationTime = NoWork;
842 }
843
844 + let didFatal = false;
845 +
846 startWorkLoopTimer(nextUnitOfWork);
847
811 - let didError = false;
812 - let error = null;
813 - if (__DEV__) {
814 - invokeGuardedCallback(null, workLoop, null, isAsync);
815 - if (hasCaughtError()) {
816 - didError = true;
817 - error = clearCaughtError();
818 - }
819 - } else {
848 + do {
849 try {
850 workLoop(isAsync);
822 - } catch (e) {
823 - didError = true;
824 - error = e;
825 - }
826 - }
827 -
828 - // An error was thrown during the render phase.
829 - while (didError) {
830 - if (didFatal) {
831 - // This was a fatal error. Don't attempt to recover from it.
832 - firstUncaughtError = error;
833 - break;
834 - }
835 -
836 - const failedWork = nextUnitOfWork;
837 - if (failedWork === null) {
838 - // An error was thrown but there's no current unit of work. This can
839 - // happen during the commit phase if there's a bug in the renderer.
840 - didFatal = true;
841 - continue;
842 - }
843 -
844 - // "Capture" the error by finding the nearest boundary. If there is no
845 - // error boundary, we use the root.
846 - const boundary = captureError(failedWork, error);
847 - invariant(
848 - boundary !== null,
849 - 'Should have found an error boundary. This error is likely ' +
850 - 'caused by a bug in React. Please file an issue.',
851 - );
852 -
853 - if (didFatal) {
854 - // The error we just captured was a fatal error. This happens
855 - // when the error propagates to the root more than once.
856 - continue;
857 - }
851 + } catch (thrownValue) {
852 + if (nextUnitOfWork === null) {
853 + // This is a fatal error.
854 + didFatal = true;
855 + onUncaughtError(thrownValue);
856 + break;
857 + }
858
859 - didError = false;
860 - error = null;
861 - if (__DEV__) {
862 - invokeGuardedCallback(
863 - null,
864 - renderRootCatchBlock,
865 - null,
866 - root,
867 - failedWork,
868 - boundary,
869 - isAsync,
870 - );
871 - if (hasCaughtError()) {
872 - didError = true;
873 - error = clearCaughtError();
874 - continue;
859 + if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
860 + const failedUnitOfWork = nextUnitOfWork;
861 + replayUnitOfWork(failedUnitOfWork, isAsync);
862 }
876 - } else {
877 - try {
878 - renderRootCatchBlock(root, failedWork, boundary, isAsync);
879 - error = null;
880 - } catch (e) {
881 - didError = true;
882 - error = e;
883 - continue;
863 +
864 + const sourceFiber: Fiber = nextUnitOfWork;
865 + let returnFiber = sourceFiber.return;
866 + if (returnFiber === null) {
867 + // This is a fatal error.
868 + didFatal = true;
869 + onUncaughtError(thrownValue);
870 + break;
871 }
872 + throwException(returnFiber, sourceFiber, thrownValue);
873 + nextUnitOfWork = completeUnitOfWork(sourceFiber);
874 }
886 - // We're finished working. Exit the error loop.
875 break;
888 - }
889 -
890 - const uncaughtError = firstUncaughtError;
876 + } while (true);
877
878 // We're done performing work. Time to clean up.
879 stopWorkLoopTimer(interruptedBy);
880 interruptedBy = null;
881 isWorking = false;
896 - didFatal = false;
897 - firstUncaughtError = null;
898 -
899 - if (uncaughtError !== null) {
900 - onUncaughtError(uncaughtError);
901 - }
902 -
903 - return root.isReadyForCommit ? root.current.alternate : null;
904 - }
905 -
906 - // Returns the boundary that captured the error, or null if the error is ignored
907 - function captureError(failedWork: Fiber, error: mixed): Fiber | null {
908 - // It is no longer valid because we exited the user code.
909 - ReactCurrentOwner.current = null;
910 - if (__DEV__) {
911 - ReactDebugCurrentFiber.resetCurrentFiber();
912 - }
913 -
914 - // Search for the nearest error boundary.
915 - let boundary: Fiber | null = null;
916 -
917 - // Passed to logCapturedError()
918 - let errorBoundaryFound: boolean = false;
919 - let willRetry: boolean = false;
920 - let errorBoundaryName: string | null = null;
921 -
922 - // Host containers are a special case. If the failed work itself is a host
923 - // container, then it acts as its own boundary. In all other cases, we
924 - // ignore the work itself and only search through the parents.
925 - if (failedWork.tag === HostRoot) {
926 - boundary = failedWork;
927 -
928 - if (isFailedBoundary(failedWork)) {
929 - // If this root already failed, there must have been an error when
930 - // attempting to unmount it. This is a worst-case scenario and
931 - // should only be possible if there's a bug in the renderer.
932 - didFatal = true;
933 - }
934 - } else {
935 - let node = failedWork.return;
936 - while (node !== null && boundary === null) {
937 - if (node.tag === ClassComponent) {
938 - const instance = node.stateNode;
939 - if (typeof instance.componentDidCatch === 'function') {
940 - errorBoundaryFound = true;
941 - errorBoundaryName = getComponentName(node);
942 -
943 - // Found an error boundary!
944 - boundary = node;
945 - willRetry = true;
946 - }
947 - } else if (node.tag === HostRoot) {
948 - // Treat the root like a no-op error boundary
949 - boundary = node;
950 - }
882
952 - if (isFailedBoundary(node)) {
953 - // This boundary is already in a failed state.
954 -
955 - // If we're currently unmounting, that means this error was
956 - // thrown while unmounting a failed subtree. We should ignore
957 - // the error.
958 - if (isUnmounting) {
959 - return null;
960 - }
961 -
962 - // If we're in the commit phase, we should check to see if
963 - // this boundary already captured an error during this commit.
964 - // This case exists because multiple errors can be thrown during
965 - // a single commit without interruption.
966 - if (
967 - commitPhaseBoundaries !== null &&
968 - (commitPhaseBoundaries.has(node) ||
969 - (node.alternate !== null &&
970 - commitPhaseBoundaries.has(node.alternate)))
971 - ) {
972 - // If so, we should ignore this error.
973 - return null;
974 - }
975 -
976 - // The error should propagate to the next boundary -— we keep looking.
977 - boundary = null;
978 - willRetry = false;
979 - }
980 -
981 - node = node.return;
982 - }
983 - }
984 -
985 - if (boundary !== null) {
986 - // Add to the collection of failed boundaries. This lets us know that
987 - // subsequent errors in this subtree should propagate to the next boundary.
988 - if (failedBoundaries === null) {
989 - failedBoundaries = new Set();
990 - }
991 - failedBoundaries.add(boundary);
992 -
993 - // This method is unsafe outside of the begin and complete phases.
994 - // We might be in the commit phase when an error is captured.
995 - // The risk is that the return path from this Fiber may not be accurate.
996 - // That risk is acceptable given the benefit of providing users more context.
997 - const componentStack = getStackAddendumByWorkInProgressFiber(failedWork);
998 - const componentName = getComponentName(failedWork);
999 -
1000 - // Add to the collection of captured errors. This is stored as a global
1001 - // map of errors and their component stack location keyed by the boundaries
1002 - // that capture them. We mostly use this Map as a Set; it's a Map only to
1003 - // avoid adding a field to Fiber to store the error.
1004 - if (capturedErrors === null) {
1005 - capturedErrors = new Map();
1006 - }
1007 -
1008 - const capturedError = {
1009 - componentName,
1010 - componentStack,
1011 - error,
1012 - errorBoundary: errorBoundaryFound ? boundary.stateNode : null,
1013 - errorBoundaryFound,
1014 - errorBoundaryName,
1015 - willRetry,
1016 - };
1017 -
1018 - capturedErrors.set(boundary, capturedError);
1019 -
1020 - try {
1021 - logCapturedError(capturedError);
1022 - } catch (e) {
1023 - // Prevent cycle if logCapturedError() throws.
1024 - // A cycle may still occur if logCapturedError renders a component that throws.
1025 - const suppressLogging = e && e.suppressReactErrorLogging;
1026 - if (!suppressLogging) {
1027 - console.error(e);
1028 - }
1029 - }
1030 -
1031 - // If we're in the commit phase, defer scheduling an update on the
1032 - // boundary until after the commit is complete
1033 - if (isCommitting) {
1034 - if (commitPhaseBoundaries === null) {
1035 - commitPhaseBoundaries = new Set();
1036 - }
1037 - commitPhaseBoundaries.add(boundary);
883 + // Yield back to main thread.
884 + if (didFatal) {
885 + // There was a fatal error.
886 + return null;
887 + } else if (nextUnitOfWork === null) {
888 + // We reached the root.
889 + if (isRootReadyForCommit) {
890 + // The root successfully completed. It's ready for commit.
891 + root.pendingCommitExpirationTime = expirationTime;
892 + const finishedWork = root.current.alternate;
893 + return finishedWork;
894 } else {
1039 - // Otherwise, schedule an update now.
1040 - // TODO: Is this actually necessary during the render phase? Is it
1041 - // possible to unwind and continue rendering at the same priority,
1042 - // without corrupting internal state?
1043 - scheduleErrorRecovery(boundary);
895 + // The root did not complete.
896 + invariant(
897 + false,
898 + 'Expired work should have completed. This error is likely caused ' +
899 + 'by a bug in React. Please file an issue.',
900 + );
901 }
1045 - return boundary;
1046 - } else if (firstUncaughtError === null) {
1047 - // If no boundary is found, we'll need to throw the error
1048 - firstUncaughtError = error;
902 + } else {
903 + // There's more work to do, but we ran out of time. Yield back to
904 + // the renderer.
905 + return null;
906 }
1050 - return null;
907 }
908
1053 - function hasCapturedError(fiber: Fiber): boolean {
1054 - // TODO: capturedErrors should store the boundary instance, to avoid needing
1055 - // to check the alternate.
1056 - return (
1057 - capturedErrors !== null &&
1058 - (capturedErrors.has(fiber) ||
1059 - (fiber.alternate !== null && capturedErrors.has(fiber.alternate)))
1060 - );
909 + function scheduleCapture(sourceFiber, boundaryFiber, value, expirationTime) {
910 + // TODO: We only support dispatching errors.
911 + const capturedValue = createCapturedValue(value, sourceFiber);
912 + const update = {
913 + expirationTime,
914 + partialState: null,
915 + callback: null,
916 + isReplace: false,
917 + isForced: false,
918 + capturedValue,
919 + next: null,
920 + };
921 + insertUpdateIntoFiber(boundaryFiber, update);
922 + scheduleWork(boundaryFiber, expirationTime);
923 }
924
1063 - function isFailedBoundary(fiber: Fiber): boolean {
1064 - // TODO: failedBoundaries should store the boundary instance, to avoid
1065 - // needing to check the alternate.
1066 - return (
1067 - failedBoundaries !== null &&
1068 - (failedBoundaries.has(fiber) ||
1069 - (fiber.alternate !== null && failedBoundaries.has(fiber.alternate)))
1070 - );
1071 - }
1072 -
1073 - function commitErrorHandling(effectfulFiber: Fiber) {
1074 - let capturedError;
1075 - if (capturedErrors !== null) {
1076 - capturedError = capturedErrors.get(effectfulFiber);
1077 - capturedErrors.delete(effectfulFiber);
1078 - if (capturedError == null) {
1079 - if (effectfulFiber.alternate !== null) {
1080 - effectfulFiber = effectfulFiber.alternate;
1081 - capturedError = capturedErrors.get(effectfulFiber);
1082 - capturedErrors.delete(effectfulFiber);
1083 - }
1084 - }
1085 - }
1086 -
925 + function dispatch(
926 + sourceFiber: Fiber,
927 + value: mixed,
928 + expirationTime: ExpirationTime,
929 + ) {
930 invariant(
1088 - capturedError != null,
1089 - 'No error for given unit of work. This error is likely caused by a ' +
1090 - 'bug in React. Please file an issue.',
931 + !isWorking || isCommitting,
932 + 'dispatch: Cannot dispatch during the render phase.',
933 );
934
1093 - switch (effectfulFiber.tag) {
1094 - case ClassComponent:
1095 - const instance = effectfulFiber.stateNode;
1096 -
1097 - const info: HandleErrorInfo = {
1098 - componentStack: capturedError.componentStack,
1099 - };
935 + // TODO: Handle arrays
936
1101 - // Allow the boundary to handle the error, usually by scheduling
1102 - // an update to itself
1103 - instance.componentDidCatch(capturedError.error, info);
1104 - return;
1105 - case HostRoot:
1106 - if (firstUncaughtError === null) {
1107 - firstUncaughtError = capturedError.error;
1108 - }
1109 - return;
1110 - default:
1111 - invariant(
1112 - false,
1113 - 'Invalid type of work. This error is likely caused by a bug in ' +
1114 - 'React. Please file an issue.',
1115 - );
1116 - }
1117 - }
1118 -
1119 - function unwindContexts(from: Fiber, to: Fiber) {
1120 - let node = from;
1121 - while (node !== null) {
1122 - switch (node.tag) {
937 + let fiber = sourceFiber.return;
938 + while (fiber !== null) {
939 + switch (fiber.tag) {
940 case ClassComponent:
1124 - popContextProvider(node);
1125 - break;
1126 - case HostComponent:
1127 - popHostContext(node);
941 + const ctor = fiber.type;
942 + const instance = fiber.stateNode;
943 + if (
944 + typeof ctor.getDerivedStateFromCatch === 'function' ||
945 + (typeof instance.componentDidCatch === 'function' &&
946 + !isAlreadyFailedLegacyErrorBoundary(instance))
947 + ) {
948 + scheduleCapture(sourceFiber, fiber, value, expirationTime);
949 + return;
950 + }
951 break;
952 + // TODO: Handle async boundaries
953 case HostRoot:
1130 - popHostContainer(node);
1131 - break;
1132 - case HostPortal:
1133 - popHostContainer(node);
1134 - break;
1135 - case ContextProvider:
1136 - popProvider(node);
1137 - break;
1138 - }
1139 - if (node === to || node.alternate === to) {
1140 - stopFailedWorkTimer(node);
1141 - break;
1142 - } else {
1143 - stopWorkTimer(node);
954 + scheduleCapture(sourceFiber, fiber, value, expirationTime);
955 + return;
956 }
1145 - node = node.return;
957 + fiber = fiber.return;
958 + }
959 +
960 + if (sourceFiber.tag === HostRoot) {
961 + // Error was thrown at the root. There is no parent, so the root
962 + // itself should capture it.
963 + scheduleCapture(sourceFiber, sourceFiber, value, expirationTime);
964 }
965 }
966
1149 - function computeAsyncExpiration() {
967 + function onCommitPhaseError(fiber: Fiber, error: mixed) {
968 + return dispatch(fiber, error, Sync);
969 + }
970 +
971 + function computeAsyncExpiration(currentTime: ExpirationTime) {
972 // Given the current clock time, returns an expiration time. We use rounding
973 // to batch like updates together.
974 // Should complete within ~1000ms. 1200ms max.
1153 - const currentTime = recalculateCurrentTime();
1154 - const expirationMs = 1000;
1155 - const bucketSizeMs = 200;
975 + const expirationMs = 5000;
976 + const bucketSizeMs = 250;
977 return computeExpirationBucket(currentTime, expirationMs, bucketSizeMs);
978 }
979
1159 - function computeInteractiveExpiration() {
980 + function computeInteractiveExpiration(currentTime: ExpirationTime) {
981 // Should complete within ~500ms. 600ms max.
1161 - const currentTime = recalculateCurrentTime();
982 const expirationMs = 500;
983 const bucketSizeMs = 100;
984 return computeExpirationBucket(currentTime, expirationMs, bucketSizeMs);
@@ -1166,7 +986,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
986
987 // Creates a unique async expiration time.
988 function computeUniqueAsyncExpiration(): ExpirationTime {
1169 - let result = computeAsyncExpiration();
989 + const currentTime = recalculateCurrentTime();
990 + let result = computeAsyncExpiration(currentTime);
991 if (result <= lastUniqueAsyncExpiration) {
992 // Since we assume the current time monotonically increases, we only hit
993 // this branch when computeUniqueAsyncExpiration is fired multiple times
@@ -1198,10 +1019,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1019 if (fiber.mode & AsyncMode) {
1020 if (isBatchingInteractiveUpdates) {
1021 // This is an interactive update
1201 - expirationTime = computeInteractiveExpiration();
1022 + const currentTime = recalculateCurrentTime();
1023 + expirationTime = computeInteractiveExpiration(currentTime);
1024 } else {
1025 // This is an async update
1204 - expirationTime = computeAsyncExpiration();
1026 + const currentTime = recalculateCurrentTime();
1027 + expirationTime = computeAsyncExpiration(currentTime);
1028 }
1029 } else {
1030 // This is a sync update
@@ -1226,27 +1049,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1049 return scheduleWorkImpl(fiber, expirationTime, false);
1050 }
1051
1229 - function checkRootNeedsClearing(
1230 - root: FiberRoot,
1231 - fiber: Fiber,
1232 - expirationTime: ExpirationTime,
1233 - ) {
1234 - if (
1235 - !isWorking &&
1236 - root === nextRoot &&
1237 - expirationTime < nextRenderExpirationTime
1238 - ) {
1239 - // Restart the root from the top.
1240 - if (nextUnitOfWork !== null) {
1241 - // This is an interruption. (Used for performance tracking.)
1242 - interruptedBy = fiber;
1243 - }
1244 - nextRoot = null;
1245 - nextUnitOfWork = null;
1246 - nextRenderExpirationTime = NoWork;
1247 - }
1248 - }
1249 -
1052 function scheduleWorkImpl(
1053 fiber: Fiber,
1054 expirationTime: ExpirationTime,
@@ -1282,10 +1084,27 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1084 if (node.return === null) {
1085 if (node.tag === HostRoot) {
1086 const root: FiberRoot = (node.stateNode: any);
1285 -
1286 - checkRootNeedsClearing(root, fiber, expirationTime);
1287 - requestWork(root, expirationTime);
1288 - checkRootNeedsClearing(root, fiber, expirationTime);
1087 + if (
1088 + !isWorking &&
1089 + nextRenderExpirationTime !== NoWork &&
1090 + expirationTime < nextRenderExpirationTime
1091 + ) {
1092 + // This is an interruption. (Used for performance tracking.)
1093 + interruptedBy = fiber;
1094 + resetContextStack();
1095 + }
1096 + if (nextRoot !== root || !isWorking) {
1097 + requestWork(root, expirationTime);
1098 + }
1099 + if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
1100 + invariant(
1101 + false,
1102 + 'Maximum update depth exceeded. This can happen when a ' +
1103 + 'component repeatedly calls setState inside ' +
1104 + 'componentWillUpdate or componentDidUpdate. React limits ' +
1105 + 'the number of nested updates to prevent infinite loops.',
1106 + );
1107 + }
1108 } else {
1109 if (__DEV__) {
1110 if (!isErrorRecovery && fiber.tag === ClassComponent) {
@@ -1299,27 +1118,23 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1118 }
1119 }
1120
1302 - function scheduleErrorRecovery(fiber: Fiber) {
1303 - scheduleWorkImpl(fiber, Sync, true);
1304 - }
1305 -
1121 function recalculateCurrentTime(): ExpirationTime {
1122 // Subtract initial time so it fits inside 32bits
1308 - const ms = now() - startTime;
1309 - mostRecentCurrentTime = msToExpirationTime(ms);
1123 + mostRecentCurrentTimeMs = now() - originalStartTimeMs;
1124 + mostRecentCurrentTime = msToExpirationTime(mostRecentCurrentTimeMs);
1125 return mostRecentCurrentTime;
1126 }
1127
1128 function deferredUpdates<A>(fn: () => A): A {
1129 const previousExpirationContext = expirationContext;
1315 - expirationContext = computeAsyncExpiration();
1130 + const currentTime = recalculateCurrentTime();
1131 + expirationContext = computeAsyncExpiration(currentTime);
1132 try {
1133 return fn();
1134 } finally {
1135 expirationContext = previousExpirationContext;
1136 }
1137 }
1322 -
1138 function syncUpdates<A, B, C0, D, R>(
1139 fn: (A, B, C0, D) => R,
1140 a: A,
@@ -1383,7 +1198,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1198 }
1199
1200 // Compute a timeout for the given expiration time.
1386 - const currentMs = now() - startTime;
1201 + const currentMs = now() - originalStartTimeMs;
1202 const expirationMs = expirationTimeToMs(expirationTime);
1203 const timeout = expirationMs - currentMs;
1204
@@ -1394,16 +1209,35 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1209 // requestWork is called by the scheduler whenever a root receives an update.
1210 // It's up to the renderer to call renderRoot at some point in the future.
1211 function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
1397 - if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
1398 - invariant(
1399 - false,
1400 - 'Maximum update depth exceeded. This can happen when a ' +
1401 - 'component repeatedly calls setState inside componentWillUpdate or ' +
1402 - 'componentDidUpdate. React limits the number of nested updates to ' +
1403 - 'prevent infinite loops.',
1404 - );
1212 + addRootToSchedule(root, expirationTime);
1213 +
1214 + if (isRendering) {
1215 + // Prevent reentrancy. Remaining work will be scheduled at the end of
1216 + // the currently rendering batch.
1217 + return;
1218 }
1219
1220 + if (isBatchingUpdates) {
1221 + // Flush work at the end of the batch.
1222 + if (isUnbatchingUpdates) {
1223 + // ...unless we're inside unbatchedUpdates, in which case we should
1224 + // flush it now.
1225 + nextFlushedRoot = root;
1226 + nextFlushedExpirationTime = Sync;
1227 + performWorkOnRoot(root, Sync, false);
1228 + }
1229 + return;
1230 + }
1231 +
1232 + // TODO: Get rid of Sync and use current time?
1233 + if (expirationTime === Sync) {
1234 + performSyncWork();
1235 + } else {
1236 + scheduleCallbackWithExpiration(expirationTime);
1237 + }
1238 + }
1239 +
1240 + function addRootToSchedule(root: FiberRoot, expirationTime: ExpirationTime) {
1241 // Add the root to the schedule.
1242 // Check if this root is already part of the schedule.
1243 if (root.nextScheduledRoot === null) {
@@ -1428,37 +1262,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1262 root.remainingExpirationTime = expirationTime;
1263 }
1264 }
1431 -
1432 - if (isRendering) {
1433 - // Prevent reentrancy. Remaining work will be scheduled at the end of
1434 - // the currently rendering batch.
1435 - return;
1436 - }
1437 -
1438 - if (isBatchingUpdates) {
1439 - if (isUnbatchingUpdates) {
1440 - // Flush work at the end of the batch.
1441 - // ...unless we're inside unbatchedUpdates, in which case we should
1442 - // flush it now.
1443 - nextFlushedRoot = root;
1444 - nextFlushedExpirationTime = Sync;
1445 - performWorkOnRoot(root, Sync, false);
1446 - }
1447 - return;
1448 - }
1449 -
1450 - // TODO: Get rid of Sync and use current time?
1451 - if (expirationTime === Sync) {
1452 - performSyncWork();
1453 - } else {
1454 - scheduleCallbackWithExpiration(expirationTime);
1455 - }
1265 }
1266
1267 function findHighestPriorityRoot() {
1268 let highestPriorityWork = NoWork;
1269 let highestPriorityRoot = null;
1461 -
1270 if (lastScheduledRoot !== null) {
1271 let previousScheduledRoot = lastScheduledRoot;
1272 let root = firstScheduledRoot;
@@ -1520,7 +1328,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1328 const previousFlushedRoot = nextFlushedRoot;
1329 if (
1330 previousFlushedRoot !== null &&
1523 - previousFlushedRoot === highestPriorityRoot
1331 + previousFlushedRoot === highestPriorityRoot &&
1332 + highestPriorityWork === Sync
1333 ) {
1334 nestedUpdateCount++;
1335 } else {
@@ -1599,7 +1408,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1408 // Clean-up.
1409 deadline = null;
1410 deadlineDidExpire = false;
1602 - nestedUpdateCount = 0;
1411
1412 finishRendering();
1413 }
@@ -1618,6 +1426,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1426 }
1427
1428 function finishRendering() {
1429 + nestedUpdateCount = 0;
1430 +
1431 if (completedBatches !== null) {
1432 const batches = completedBatches;
1433 completedBatches = null;
@@ -1739,8 +1549,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1549 return true;
1550 }
1551
1742 - // TODO: Not happy about this hook. Conceptually, renderRoot should return a
1743 - // tuple of (isReadyForCommit, didError, error)
1552 function onUncaughtError(error) {
1553 invariant(
1554 nextFlushedRoot !== null,
@@ -1857,6 +1665,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1665 }
1666
1667 return {
1668 + recalculateCurrentTime,
1669 computeExpirationForFiber,
1670 scheduleWork,
1671 requestWork,
packages/react-reconciler/src/ReactFiberUnwindWork.js new
+139
@@ -0,0 +1,139 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
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 + */
8 +
9 +import {createCapturedValue} from './ReactCapturedValue';
10 +import {ensureUpdateQueues} from './ReactFiberUpdateQueue';
11 +
12 +import {
13 + ClassComponent,
14 + HostRoot,
15 + HostComponent,
16 + HostPortal,
17 + ContextProvider,
18 +} from 'shared/ReactTypeOfWork';
19 +import {
20 + NoEffect,
21 + DidCapture,
22 + Incomplete,
23 + ShouldCapture,
24 +} from 'shared/ReactTypeOfSideEffect';
25 +
26 +import {enableGetDerivedStateFromCatch} from 'shared/ReactFeatureFlags';
27 +
28 +import {
29 + popContextProvider as popLegacyContextProvider,
30 + popTopLevelContextObject as popTopLevelLegacyContextObject,
31 +} from './ReactFiberContext';
32 +import {popProvider} from './ReactFiberNewContext';
33 +
34 +export default function(
35 + hostContext: HostContext<C, CX>,
36 + scheduleWork: (
37 + fiber: Fiber,
38 + startTime: ExpirationTime,
39 + expirationTime: ExpirationTime,
40 + ) => void,
41 + isAlreadyFailedLegacyErrorBoundary: (instance: mixed) => boolean,
42 +) {
43 + const {popHostContainer, popHostContext} = hostContext;
44 +
45 + function throwException(
46 + returnFiber: Fiber,
47 + sourceFiber: Fiber,
48 + rawValue: mixed,
49 + ) {
50 + // The source fiber did not complete.
51 + sourceFiber.effectTag |= Incomplete;
52 + // Its effect list is no longer valid.
53 + sourceFiber.firstEffect = sourceFiber.lastEffect = null;
54 +
55 + const value = createCapturedValue(rawValue, sourceFiber);
56 +
57 + let workInProgress = returnFiber;
58 + do {
59 + switch (workInProgress.tag) {
60 + case HostRoot: {
61 + // Uncaught error
62 + const errorInfo = value;
63 + ensureUpdateQueues(workInProgress);
64 + const updateQueue: UpdateQueue = (workInProgress.updateQueue: any);
65 + updateQueue.capturedValues = [errorInfo];
66 + workInProgress.effectTag |= ShouldCapture;
67 + return;
68 + }
69 + case ClassComponent:
70 + // Capture and retry
71 + const ctor = workInProgress.type;
72 + const instance = workInProgress.stateNode;
73 + if (
74 + (workInProgress.effectTag & DidCapture) === NoEffect &&
75 + ((typeof ctor.getDerivedStateFromCatch === 'function' &&
76 + enableGetDerivedStateFromCatch) ||
77 + (instance !== null &&
78 + typeof instance.componentDidCatch === 'function' &&
79 + !isAlreadyFailedLegacyErrorBoundary(instance)))
80 + ) {
81 + ensureUpdateQueues(workInProgress);
82 + const updateQueue: UpdateQueue = (workInProgress.updateQueue: any);
83 + const capturedValues = updateQueue.capturedValues;
84 + if (capturedValues === null) {
85 + updateQueue.capturedValues = [value];
86 + } else {
87 + capturedValues.push(value);
88 + }
89 + workInProgress.effectTag |= ShouldCapture;
90 + return;
91 + }
92 + break;
93 + default:
94 + break;
95 + }
96 + workInProgress = workInProgress.return;
97 + } while (workInProgress !== null);
98 + }
99 +
100 + function unwindWork(workInProgress) {
101 + switch (workInProgress.tag) {
102 + case ClassComponent: {
103 + popLegacyContextProvider(workInProgress);
104 + const effectTag = workInProgress.effectTag;
105 + if (effectTag & ShouldCapture) {
106 + workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
107 + return workInProgress;
108 + }
109 + return null;
110 + }
111 + case HostRoot: {
112 + popHostContainer(workInProgress);
113 + popTopLevelLegacyContextObject(workInProgress);
114 + const effectTag = workInProgress.effectTag;
115 + if (effectTag & ShouldCapture) {
116 + workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
117 + return workInProgress;
118 + }
119 + return null;
120 + }
121 + case HostComponent: {
122 + popHostContext(workInProgress);
123 + return null;
124 + }
125 + case HostPortal:
126 + popHostContainer(workInProgress);
127 + return null;
128 + case ContextProvider:
129 + popProvider(workInProgress);
130 + return null;
131 + default:
132 + return null;
133 + }
134 + }
135 + return {
136 + throwException,
137 + unwindWork,
138 + };
139 +}
packages/react-reconciler/src/ReactFiberUpdateQueue.js
+45 -12
@@ -9,6 +9,7 @@
9
10 import type {Fiber} from './ReactFiber';
11 import type {ExpirationTime} from './ReactFiberExpirationTime';
12 +import type {CapturedValue} from './ReactCapturedValue';
13
14 import {
15 debugRenderPhaseSideEffects,
@@ -41,6 +42,7 @@ export type Update<State> = {
42 callback: Callback | null,
43 isReplace: boolean,
44 isForced: boolean,
45 + capturedValue: CapturedValue<mixed> | null,
46 next: Update<State> | null,
47 };
48
@@ -68,6 +70,7 @@ export type UpdateQueue<State> = {
70 callbackList: Array<Update<State>> | null,
71 hasForceUpdate: boolean,
72 isInitialized: boolean,
73 + capturedValues: Array<CapturedValue<mixed>> | null,
74
75 // Dev only
76 isProcessing?: boolean,
@@ -82,6 +85,7 @@ function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
85 callbackList: null,
86 hasForceUpdate: false,
87 isInitialized: false,
88 + capturedValues: null,
89 };
90 if (__DEV__) {
91 queue.isProcessing = false;
@@ -109,10 +113,10 @@ export function insertUpdateIntoQueue<State>(
113 }
114 }
115
112 -export function insertUpdateIntoFiber<State>(
113 - fiber: Fiber,
114 - update: Update<State>,
115 -): void {
116 +let q1;
117 +let q2;
118 +export function ensureUpdateQueues(fiber: Fiber) {
119 + q1 = q2 = null;
120 // We'll have at least one and at most two distinct update queues.
121 const alternateFiber = fiber.alternate;
122 let queue1 = fiber.updateQueue;
@@ -135,6 +139,19 @@ export function insertUpdateIntoFiber<State>(
139 }
140 queue2 = queue2 !== queue1 ? queue2 : null;
141
142 + // Use module variables instead of returning a tuple
143 + q1 = queue1;
144 + q2 = queue2;
145 +}
146 +
147 +export function insertUpdateIntoFiber<State>(
148 + fiber: Fiber,
149 + update: Update<State>,
150 +): void {
151 + ensureUpdateQueues(fiber);
152 + const queue1: Fiber = (q1: any);
153 + const queue2: Fiber | null = (q2: any);
154 +
155 // Warn if an update is scheduled from inside an updater function.
156 if (__DEV__) {
157 if (
@@ -174,14 +191,17 @@ export function insertUpdateIntoFiber<State>(
191 }
192
193 export function getUpdateExpirationTime(fiber: Fiber): ExpirationTime {
177 - if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) {
178 - return NoWork;
179 - }
180 - const updateQueue = fiber.updateQueue;
181 - if (updateQueue === null) {
182 - return NoWork;
194 + switch (fiber.tag) {
195 + case HostRoot:
196 + case ClassComponent:
197 + const updateQueue = fiber.updateQueue;
198 + if (updateQueue === null) {
199 + return NoWork;
200 + }
201 + return updateQueue.expirationTime;
202 + default:
203 + return NoWork;
204 }
184 - return updateQueue.expirationTime;
205 }
206
207 function getStateFromUpdate(update, instance, prevState, props) {
@@ -210,6 +230,7 @@ export function processUpdateQueue<State>(
230 first: currentQueue.first,
231 last: currentQueue.last,
232 isInitialized: currentQueue.isInitialized,
233 + capturedValues: currentQueue.capturedValues,
234 // These fields are no longer valid because they were already committed.
235 // Reset them.
236 callbackList: null,
@@ -311,12 +332,24 @@ export function processUpdateQueue<State>(
332 }
333 callbackList.push(update);
334 }
335 + if (update.capturedValue !== null) {
336 + let capturedValues = queue.capturedValues;
337 + if (capturedValues === null) {
338 + queue.capturedValues = [update.capturedValue];
339 + } else {
340 + capturedValues.push(update.capturedValue);
341 + }
342 + }
343 update = update.next;
344 }
345
346 if (queue.callbackList !== null) {
347 workInProgress.effectTag |= CallbackEffect;
319 - } else if (queue.first === null && !queue.hasForceUpdate) {
348 + } else if (
349 + queue.first === null &&
350 + !queue.hasForceUpdate &&
351 + queue.capturedValues === null
352 + ) {
353 // The queue is empty. We can reset it.
354 workInProgress.updateQueue = null;
355 }
packages/react-reconciler/src/__tests__/ReactExpiration-test.js
+2 -2
@@ -32,8 +32,8 @@ describe('ReactExpiration', () => {
32 ReactNoop.flushExpired();
33 expect(ReactNoop.getChildren()).toEqual([]);
34
35 - // Advance by 300ms, not enough to expire the low pri update.
36 - ReactNoop.expire(300);
35 + // Advance time a bit, but not enough to expire the low pri update.
36 + ReactNoop.expire(4500);
37 ReactNoop.flushExpired();
38 expect(ReactNoop.getChildren()).toEqual([]);
39
packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js
+4
@@ -174,6 +174,7 @@ describe('ReactIncremental', () => {
174 // This will abort the previous work and restart
175 ReactNoop.flushSync(() => ReactNoop.render(null));
176 ReactNoop.render(<Foo text="baz" />);
177 + ReactNoop.clearYields();
178
179 // Flush part of the new work
180 ReactNoop.flushDeferredPri(20 + 5);
@@ -2757,10 +2758,13 @@ describe('ReactIncremental', () => {
2758 expect(
2759 ReactNoop.flushSync(() => ReactNoop.render(<Parent step={2} />)),
2760 ).toEqual(['Parent: 2', 'Child: 2']);
2761 + ReactNoop.clearYields();
2762
2763 expect(ReactNoop.flush()).toEqual([]);
2764 });
2765
2766 + // We don't currently use fibers as keys. Re-enable this test if we
2767 + // ever do again.
2768 it('does not break with a bad Map polyfill', () => {
2769 const realMapSet = Map.prototype.set;
2770
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+335 -11
@@ -19,7 +19,9 @@ describe('ReactIncrementalErrorHandling', () => {
19 beforeEach(() => {
20 jest.resetModules();
21 ReactFeatureFlags = require('shared/ReactFeatureFlags');
22 + ReactFeatureFlags.enableGetDerivedStateFromCatch = true;
23 ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
24 + ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
25 PropTypes = require('prop-types');
26 React = require('react');
27 ReactNoop = require('react-noop-renderer');
@@ -34,6 +36,211 @@ describe('ReactIncrementalErrorHandling', () => {
36 return {type: 'span', children: [], prop};
37 }
38
39 + it('recovers from errors asynchronously', () => {
40 + class ErrorBoundary extends React.Component {
41 + state = {error: null};
42 + componentDidCatch(error) {
43 + ReactNoop.yield('componentDidCatch');
44 + this.setState({error});
45 + }
46 + render() {
47 + if (this.state.error) {
48 + ReactNoop.yield('ErrorBoundary (catch)');
49 + return <ErrorMessage error={this.state.error} />;
50 + }
51 + ReactNoop.yield('ErrorBoundary (try)');
52 + return this.props.children;
53 + }
54 + }
55 +
56 + function ErrorMessage(props) {
57 + ReactNoop.yield('ErrorMessage');
58 + return <span prop={`Caught an error: ${props.error.message}`} />;
59 + }
60 +
61 + function Indirection(props) {
62 + ReactNoop.yield('Indirection');
63 + return props.children;
64 + }
65 +
66 + function BadRender() {
67 + ReactNoop.yield('throw');
68 + throw new Error('oops!');
69 + }
70 +
71 + ReactNoop.render(
72 + <ErrorBoundary>
73 + <Indirection>
74 + <Indirection>
75 + <Indirection>
76 + <BadRender />
77 + </Indirection>
78 + </Indirection>
79 + </Indirection>
80 + </ErrorBoundary>,
81 + );
82 +
83 + ReactNoop.flushThrough([
84 + 'ErrorBoundary (try)',
85 + 'Indirection',
86 + 'Indirection',
87 + 'Indirection',
88 + // The error was thrown, but React ran out of time and yielded
89 + // before recovering.
90 + 'throw',
91 + ]);
92 +
93 + // Upon resuming, componentDidCatch is called
94 + ReactNoop.flushThrough([
95 + 'componentDidCatch',
96 + 'ErrorBoundary (catch)',
97 + 'ErrorMessage',
98 + ]);
99 + expect(ReactNoop.getChildren()).toEqual([span('Caught an error: oops!')]);
100 + });
101 +
102 + it('can recover from an error within a single render phase', () => {
103 + class Sibling extends React.Component {
104 + componentWillUnmount() {
105 + ReactNoop.yield('Unmount Sibling');
106 + }
107 + render() {
108 + return null;
109 + }
110 + }
111 +
112 + class ErrorBoundary extends React.Component {
113 + state = {error: null};
114 + static getDerivedStateFromCatch(error) {
115 + ReactNoop.yield('getDerivedStateFromCatch');
116 + return {error};
117 + }
118 + render() {
119 + if (this.state.error) {
120 + ReactNoop.yield('ErrorBoundary (catch)');
121 + } else {
122 + ReactNoop.yield('ErrorBoundary (try)');
123 + }
124 + return (
125 + <React.Fragment>
126 + <Sibling />
127 + {this.state.error ? (
128 + <ErrorMessage error={this.state.error} />
129 + ) : (
130 + this.props.children
131 + )}
132 + </React.Fragment>
133 + );
134 + }
135 + }
136 +
137 + function ErrorMessage(props) {
138 + ReactNoop.yield('ErrorMessage');
139 + return <span prop={`Caught an error: ${props.error.message}`} />;
140 + }
141 +
142 + function Indirection(props) {
143 + ReactNoop.yield('Indirection');
144 + return props.children;
145 + }
146 +
147 + function BadRender() {
148 + ReactNoop.yield('throw');
149 + throw new Error('oops!');
150 + }
151 +
152 + function App(props) {
153 + return (
154 + <ErrorBoundary>
155 + <Indirection>
156 + <Indirection>
157 + <Indirection>
158 + {props.shouldThrow ? <BadRender /> : null}
159 + </Indirection>
160 + </Indirection>
161 + </Indirection>
162 + </ErrorBoundary>
163 + );
164 + }
165 +
166 + ReactNoop.render(<App shouldThrow={false} />);
167 + ReactNoop.flush();
168 +
169 + ReactNoop.render(<App shouldThrow={true} />);
170 + ReactNoop.flushThrough([
171 + 'ErrorBoundary (try)',
172 + 'Indirection',
173 + 'Indirection',
174 + 'Indirection',
175 + // The error was thrown, but React ran out of time and yielded
176 + // before recovering.
177 + 'throw',
178 + ]);
179 +
180 + // Nothing committed yet.
181 + expect(ReactNoop.getChildren()).toEqual([]);
182 +
183 + // Upon resuming, getDerivedStateFromCatch is called
184 + ReactNoop.flushThrough([
185 + 'getDerivedStateFromCatch',
186 + 'ErrorBoundary (catch)',
187 + 'ErrorMessage',
188 + ]);
189 + // Still hasn't committed
190 + expect(ReactNoop.getChildren()).toEqual([]);
191 + // The sibling should be re-mounted because it is the child of a failed
192 + // error boundary
193 + expect(ReactNoop.flush()).toEqual(['Unmount Sibling']);
194 + expect(ReactNoop.getChildren()).toEqual([span('Caught an error: oops!')]);
195 + });
196 +
197 + it('calls componentDidCatch multiple times for multiple errors', () => {
198 + let id = 0;
199 + class BadMount extends React.Component {
200 + componentDidMount() {
201 + throw new Error(`Error ${++id}`);
202 + }
203 + render() {
204 + ReactNoop.yield('BadMount');
205 + return null;
206 + }
207 + }
208 +
209 + class ErrorBoundary extends React.Component {
210 + state = {errorCount: 0};
211 + componentDidCatch(error) {
212 + ReactNoop.yield(`componentDidCatch: ${error.message}`);
213 + this.setState(state => ({errorCount: state.errorCount + 1}));
214 + }
215 + render() {
216 + if (this.state.errorCount > 0) {
217 + return <span prop={`Number of errors: ${this.state.errorCount}`} />;
218 + }
219 + ReactNoop.yield('ErrorBoundary');
220 + return this.props.children;
221 + }
222 + }
223 +
224 + ReactNoop.render(
225 + <ErrorBoundary>
226 + <BadMount />
227 + <BadMount />
228 + <BadMount />
229 + </ErrorBoundary>,
230 + );
231 +
232 + expect(ReactNoop.flush()).toEqual([
233 + 'ErrorBoundary',
234 + 'BadMount',
235 + 'BadMount',
236 + 'BadMount',
237 + 'componentDidCatch: Error 1',
238 + 'componentDidCatch: Error 2',
239 + 'componentDidCatch: Error 3',
240 + ]);
241 + expect(ReactNoop.getChildren()).toEqual([span('Number of errors: 3')]);
242 + });
243 +
244 it('catches render error in a boundary during full deferred mounting', () => {
245 class ErrorBoundary extends React.Component {
246 state = {error: null};
@@ -64,27 +271,26 @@ describe('ReactIncrementalErrorHandling', () => {
271 });
272
273 it('catches render error in a boundary during partial deferred mounting', () => {
67 - const ops = [];
274 class ErrorBoundary extends React.Component {
275 state = {error: null};
276 componentDidCatch(error) {
71 - ops.push('ErrorBoundary componentDidCatch');
277 + ReactNoop.yield('ErrorBoundary componentDidCatch');
278 this.setState({error});
279 }
280 render() {
281 if (this.state.error) {
76 - ops.push('ErrorBoundary render error');
282 + ReactNoop.yield('ErrorBoundary render error');
283 return (
284 <span prop={`Caught an error: ${this.state.error.message}.`} />
285 );
286 }
81 - ops.push('ErrorBoundary render success');
287 + ReactNoop.yield('ErrorBoundary render success');
288 return this.props.children;
289 }
290 }
291
292 function BrokenRender(props) {
87 - ops.push('BrokenRender');
293 + ReactNoop.yield('BrokenRender');
294 throw new Error('Hello');
295 }
296
@@ -94,13 +300,10 @@ describe('ReactIncrementalErrorHandling', () => {
300 </ErrorBoundary>,
301 );
302
97 - ReactNoop.flushDeferredPri(15);
98 - expect(ops).toEqual(['ErrorBoundary render success']);
303 + ReactNoop.flushThrough(['ErrorBoundary render success']);
304 expect(ReactNoop.getChildren()).toEqual([]);
305
101 - ops.length = 0;
102 - ReactNoop.flushDeferredPri(30);
103 - expect(ops).toEqual([
306 + expect(ReactNoop.flush()).toEqual([
307 'BrokenRender',
308 'ErrorBoundary componentDidCatch',
309 'ErrorBoundary render error',
@@ -401,7 +604,6 @@ describe('ReactIncrementalErrorHandling', () => {
604 ReactNoop.flush();
605 }).toThrow('Hello');
606 expect(ops).toEqual(['BrokenRender']);
404 -
607 ops = [];
608 ReactNoop.render(<Foo />);
609 ReactNoop.flush();
@@ -935,4 +1137,126 @@ describe('ReactIncrementalErrorHandling', () => {
1137 });
1138 expect(() => ReactNoop.flush()).toThrow('Error!');
1139 });
1140 +
1141 + it('error boundaries capture non-errors', () => {
1142 + spyOnProd(console, 'error');
1143 + spyOnDev(console, 'error');
1144 + let ops = [];
1145 +
1146 + class ErrorBoundary extends React.Component {
1147 + state = {error: null};
1148 + componentDidCatch(error) {
1149 + // Should not be called
1150 + ops.push('componentDidCatch');
1151 + this.setState({error});
1152 + }
1153 + render() {
1154 + if (this.state.error) {
1155 + ops.push('ErrorBoundary (catch)');
1156 + return (
1157 + <span
1158 + prop={`Caught an error: ${this.state.error.nonStandardMessage}`}
1159 + />
1160 + );
1161 + }
1162 + ops.push('ErrorBoundary (try)');
1163 + return this.props.children;
1164 + }
1165 + }
1166 +
1167 + function Indirection(props) {
1168 + ops.push('Indirection');
1169 + return props.children;
1170 + }
1171 +
1172 + const notAnError = {nonStandardMessage: 'oops'};
1173 + function BadRender() {
1174 + ops.push('BadRender');
1175 + throw notAnError;
1176 + }
1177 +
1178 + ReactNoop.render(
1179 + <ErrorBoundary>
1180 + <Indirection>
1181 + <BadRender />
1182 + </Indirection>
1183 + </ErrorBoundary>,
1184 + );
1185 + ReactNoop.flush();
1186 +
1187 + expect(ops).toEqual([
1188 + 'ErrorBoundary (try)',
1189 + 'Indirection',
1190 + 'BadRender',
1191 + 'componentDidCatch',
1192 + 'ErrorBoundary (catch)',
1193 + ]);
1194 + expect(ReactNoop.getChildren()).toEqual([span('Caught an error: oops')]);
1195 +
1196 + if (__DEV__) {
1197 + expect(console.error.calls.count()).toBe(1);
1198 + expect(console.error.calls.argsFor(0)[0]).toContain(
1199 + 'The above error occurred in the <BadRender> component:',
1200 + );
1201 + } else {
1202 + expect(console.error.calls.count()).toBe(1);
1203 + expect(console.error.calls.argsFor(0)[0]).toBe(notAnError);
1204 + }
1205 + });
1206 +
1207 + // TODO: Error boundary does not catch promises
1208 +
1209 + it('continues working on siblings of a component that throws', () => {
1210 + class ErrorBoundary extends React.Component {
1211 + state = {error: null};
1212 + componentDidCatch(error) {
1213 + ReactNoop.yield('componentDidCatch');
1214 + this.setState({error});
1215 + }
1216 + render() {
1217 + if (this.state.error) {
1218 + ReactNoop.yield('ErrorBoundary (catch)');
1219 + return <ErrorMessage error={this.state.error} />;
1220 + }
1221 + ReactNoop.yield('ErrorBoundary (try)');
1222 + return this.props.children;
1223 + }
1224 + }
1225 +
1226 + function ErrorMessage(props) {
1227 + ReactNoop.yield('ErrorMessage');
1228 + return <span prop={`Caught an error: ${props.error.message}`} />;
1229 + }
1230 +
1231 + function BadRenderSibling(props) {
1232 + ReactNoop.yield('BadRenderSibling');
1233 + return null;
1234 + }
1235 +
1236 + function BadRender() {
1237 + ReactNoop.yield('throw');
1238 + throw new Error('oops!');
1239 + }
1240 +
1241 + ReactNoop.render(
1242 + <ErrorBoundary>
1243 + <BadRender />
1244 + <BadRenderSibling />
1245 + <BadRenderSibling />
1246 + </ErrorBoundary>,
1247 + );
1248 +
1249 + expect(ReactNoop.flush()).toEqual([
1250 + 'ErrorBoundary (try)',
1251 + 'throw',
1252 + // Continue rendering siblings after BadRender throws
1253 + 'BadRenderSibling',
1254 + 'BadRenderSibling',
1255 + // Recover from the error
1256 + 'componentDidCatch',
1257 + 'ErrorBoundary (catch)',
1258 + 'ErrorMessage',
1259 + ]);
1260 + expect(ReactNoop.getChildren()).toEqual([span('Caught an error: oops!')]);
1261 + });
1262 });
packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.internal.js
+27 -88
@@ -19,6 +19,7 @@ describe('ReactIncrementalErrorLogging', () => {
19 jest.resetModules();
20 ReactFeatureFlags = require('shared/ReactFeatureFlags');
21 ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
22 + ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
23 React = require('react');
24 ReactNoop = require('react-noop-renderer');
25 });
@@ -153,104 +154,42 @@ describe('ReactIncrementalErrorLogging', () => {
154 }
155 });
156
156 - it('should relay info about error boundary and retry attempts if applicable', () => {
157 - // Errors are redundantly logged in production mode by ReactFiberErrorLogger.
158 - // It's okay to ignore them for the purpose of this test.
159 - spyOnProd(console, 'error');
160 -
161 - class ParentComponent extends React.Component {
162 - render() {
163 - return <ErrorBoundaryComponent />;
164 - }
165 - }
166 -
167 - let handleErrorCalls = [];
168 - let renderAttempts = 0;
169 -
170 - class ErrorBoundaryComponent extends React.Component {
157 + it('resets instance variables before unmounting failed node', () => {
158 + class ErrorBoundary extends React.Component {
159 + state = {error: null};
160 componentDidCatch(error) {
172 - handleErrorCalls.push(error);
173 - this.setState({}); // Render again
161 + this.setState({error});
162 }
163 render() {
176 - return <ErrorThrowingComponent />;
164 + return this.state.error ? null : this.props.children;
165 }
166 }
179 -
180 - class ErrorThrowingComponent extends React.Component {
167 + class Foo extends React.Component {
168 + state = {step: 0};
169 componentDidMount() {
182 - const error = new Error('componentDidMount error');
183 - // Note: it's `true` on the Error prototype our test environment.
184 - // That lets us avoid asserting on warnings for each expected error.
185 - // Here we intentionally shadow it to test logging, like in real apps.
186 - error.suppressReactErrorLogging = undefined;
187 - throw error;
170 + this.setState({step: 1});
171 + }
172 + componentWillUnmount() {
173 + ReactNoop.yield('componentWillUnmount: ' + this.state.step);
174 }
175 render() {
190 - renderAttempts++;
191 - return <div />;
176 + ReactNoop.yield('render: ' + this.state.step);
177 + if (this.state.step > 0) {
178 + throw new Error('oops');
179 + }
180 + return null;
181 }
182 }
183
195 - ReactNoop.render(<ParentComponent />);
196 -
197 - expect(() => {
198 - expect(ReactNoop.flush).toWarnDev([
199 - 'The above error occurred in the <ErrorThrowingComponent> component:\n' +
200 - ' in ErrorThrowingComponent (at **)\n' +
201 - ' in ErrorBoundaryComponent (at **)\n' +
202 - ' in ParentComponent (at **)\n\n' +
203 - 'React will try to recreate this component tree from scratch ' +
204 - 'using the error boundary you provided, ErrorBoundaryComponent.',
205 - 'The above error occurred in the <ErrorThrowingComponent> component:\n' +
206 - ' in ErrorThrowingComponent (at **)\n' +
207 - ' in ErrorBoundaryComponent (at **)\n' +
208 - ' in ParentComponent (at **)\n\n' +
209 - 'This error was initially handled by the error boundary ErrorBoundaryComponent.\n' +
210 - 'Recreating the tree from scratch failed so React will unmount the tree.',
211 - ]);
212 - }).toThrowError('componentDidMount error');
213 -
214 - expect(renderAttempts).toBe(2);
215 - expect(handleErrorCalls.length).toBe(1);
184 + ReactNoop.render(
185 + <ErrorBoundary>
186 + <Foo />
187 + </ErrorBoundary>,
188 + );
189 + expect(ReactNoop.flush()).toEqual([
190 + 'render: 0',
191 + 'render: 1',
192 + 'componentWillUnmount: 0',
193 + ]);
194 });
195 });
218 -
219 -it('resets instance variables before unmounting failed node', () => {
220 - class ErrorBoundary extends React.Component {
221 - state = {error: null};
222 - componentDidCatch(error) {
223 - this.setState({error});
224 - }
225 - render() {
226 - return this.state.error ? null : this.props.children;
227 - }
228 - }
229 - class Foo extends React.Component {
230 - state = {step: 0};
231 - componentDidMount() {
232 - this.setState({step: 1});
233 - }
234 - componentWillUnmount() {
235 - ReactNoop.yield('componentWillUnmount: ' + this.state.step);
236 - }
237 - render() {
238 - ReactNoop.yield('render: ' + this.state.step);
239 - if (this.state.step > 0) {
240 - throw new Error('oops');
241 - }
242 - return null;
243 - }
244 - }
245 -
246 - ReactNoop.render(
247 - <ErrorBoundary>
248 - <Foo />
249 - </ErrorBoundary>,
250 - );
251 - expect(ReactNoop.flush()).toEqual([
252 - 'render: 0',
253 - 'render: 1',
254 - 'componentWillUnmount: 0',
255 - ]);
256 -});
packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js
+2 -1
@@ -116,6 +116,7 @@ describe('ReactDebugFiberPerf', () => {
116 global.performance = createUserTimingPolyfill();
117
118 require('shared/ReactFeatureFlags').enableUserTimingAPI = true;
119 + require('shared/ReactFeatureFlags').replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
120
121 // Import after the polyfill is set up:
122 React = require('react');
@@ -566,7 +567,7 @@ describe('ReactDebugFiberPerf', () => {
567 }
568
569 ReactNoop.render(<Foo />);
569 - ReactNoop.expire(5000);
570 + ReactNoop.expire(6000);
571 ReactNoop.flush();
572 expect(getFlameChart()).toMatchSnapshot();
573 });
packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.internal.js
+1
@@ -221,6 +221,7 @@ describe('ReactIncrementalScheduling', () => {
221 expect(ReactNoop.getChildren()).toEqual([span(0)]);
222
223 // Now flush the cDM update.
224 + ReactNoop.clearYields();
225 expect(ReactNoop.flush()).toEqual(['render: 1', 'componentDidUpdate: 1']);
226 expect(ReactNoop.getChildren()).toEqual([span(1)]);
227
packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.internal.js
+2
@@ -177,6 +177,7 @@ describe('ReactIncrementalUpdates', () => {
177 // Now flush the remaining work. Even though e and f were already processed,
178 // they should be processed again, to ensure that the terminal state
179 // is deterministic.
180 + ReactNoop.clearYields();
181 expect(ReactNoop.flush()).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
182 expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]);
183 });
@@ -235,6 +236,7 @@ describe('ReactIncrementalUpdates', () => {
236 // Now flush the remaining work. Even though e and f were already processed,
237 // they should be processed again, to ensure that the terminal state
238 // is deterministic.
239 + ReactNoop.clearYields();
240 expect(ReactNoop.flush()).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
241 expect(ReactNoop.getChildren()).toEqual([span('fg')]);
242 });
packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap
+7 -2
@@ -224,11 +224,14 @@ exports[`ReactDebugFiberPerf recovers from caught errors 1`] = `
224 ⛔ Boundary [mount] Warning: An error was thrown inside this error boundary
225 ⚛ Parent [mount]
226 ⚛ Baddie [mount]
227 +
228 +⚛ (React Tree Reconciliation)
229 + ⚛ Parent [mount]
230 ⚛ Boundary [mount]
231
232 ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
233 ⚛ (Committing Host Effects: 2 Total)
231 - ⚛ (Calling Lifecycle Methods: 1 Total)
234 + ⚛ (Calling Lifecycle Methods: 0 Total)
235
236 ⚛ (React Tree Reconciliation)
237 ⚛ Boundary [update]
@@ -248,9 +251,11 @@ exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = `
251 ⚛ Parent [mount]
252 ⚛ Baddie [mount]
253
254 +⚛ (React Tree Reconciliation)
255 +
256 ⚛ (Committing Changes)
257 ⚛ (Committing Host Effects: 1 Total)
253 - ⚛ (Calling Lifecycle Methods: 1 Total)
258 + ⚛ (Calling Lifecycle Methods: 0 Total)
259
260 ⚛ (Waiting for async callback...)
261
packages/react-reconciler/src/forks/ReactFiberErrorDialog.native.js
+1 -1
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import type {CapturedError} from '../ReactFiberScheduler';
10 +import type {CapturedError} from '../ReactCapturedValue';
11
12 // Module provided by RN:
13 import ExceptionsManager from 'ExceptionsManager';
packages/react-reconciler/src/forks/ReactFiberErrorDialog.www.js
+1 -1
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import type {CapturedError} from '../ReactFiberScheduler';
10 +import type {CapturedError} from '../ReactCapturedValue';
11
12 import invariant from 'fbjs/lib/invariant';
13
packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.internal.js renamed
+4
@@ -10,6 +10,8 @@
10
11 'use strict';
12
13 +const ReactFeatureFlags = require('shared/ReactFeatureFlags');
14 +ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
15 const React = require('react');
16 const ReactTestRenderer = require('react-test-renderer');
17 const prettyFormat = require('pretty-format');
@@ -438,6 +440,7 @@ describe('ReactTestRenderer', () => {
440 /* do nothing */
441 }
442 componentDidCatch() {
443 + log.push('Boundary componentDidCatch');
444 this.setState({error: true});
445 }
446 }
@@ -452,6 +455,7 @@ describe('ReactTestRenderer', () => {
455 'Boundary render',
456 'Angry render',
457 'Boundary componentDidMount',
458 + 'Boundary componentDidCatch',
459 'Boundary render',
460 ]);
461 });
packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee
+5 -1
@@ -51,7 +51,11 @@ describe 'ReactCoffeeScriptClass', ->
51 expect(->
52 ReactDOM.render React.createElement(Foo), container
53 ).toThrow()
54 - ).toWarnDev('No `render` method found on the returned component instance')
54 + ).toWarnDev([
55 + # A failed component renders twice in DEV
56 + 'No `render` method found on the returned component instance',
57 + 'No `render` method found on the returned component instance',
58 + ])
59 undefined
60
61 it 'renders a simple stateless component with prop', ->
packages/react/src/__tests__/ReactES6Class-test.js
+5 -2
@@ -59,10 +59,13 @@ describe('ReactES6Class', () => {
59 class Foo extends React.Component {}
60 expect(() =>
61 expect(() => ReactDOM.render(<Foo />, container)).toThrow(),
62 - ).toWarnDev(
62 + ).toWarnDev([
63 + // A failed component renders twice in DEV
64 'Warning: Foo(...): No `render` method found on the returned component ' +
65 'instance: you may have forgotten to define `render`.',
65 - );
66 + 'Warning: Foo(...): No `render` method found on the returned component ' +
67 + 'instance: you may have forgotten to define `render`.',
68 + ]);
69 });
70
71 it('renders a simple stateless component with prop', () => {
packages/react/src/__tests__/ReactElementValidator-test.internal.js renamed
+3
@@ -13,6 +13,7 @@
13 // classic JS without JSX.
14
15 let PropTypes;
16 +let ReactFeatureFlags;
17 let React;
18 let ReactDOM;
19 let ReactTestUtils;
@@ -24,6 +25,8 @@ describe('ReactElementValidator', () => {
25 jest.resetModules();
26
27 PropTypes = require('prop-types');
28 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
29 + ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
30 React = require('react');
31 ReactDOM = require('react-dom');
32 ReactTestUtils = require('react-dom/test-utils');
packages/react/src/__tests__/ReactStrictMode-test.internal.js
-51
@@ -531,57 +531,6 @@ describe('ReactStrictMode', () => {
531 rendered.update(<AsyncRoot foo={false} />);
532 });
533
534 - it('should not warn about uncommitted lifecycles in the event of an error', () => {
535 - let caughtError;
536 -
537 - class AsyncRoot extends React.Component {
538 - render() {
539 - return (
540 - <React.unstable_AsyncMode>
541 - <ErrorBoundary />
542 - </React.unstable_AsyncMode>
543 - );
544 - }
545 - }
546 - class ErrorBoundary extends React.Component {
547 - state = {
548 - error: null,
549 - };
550 - componentDidCatch(error) {
551 - caughtError = error;
552 - this.setState({error});
553 - }
554 - render() {
555 - return this.state.error ? <Bar /> : <Foo />;
556 - }
557 - }
558 - class Foo extends React.Component {
559 - UNSAFE_componentWillMount() {}
560 - render() {
561 - throw Error('whoops');
562 - }
563 - }
564 - class Bar extends React.Component {
565 - UNSAFE_componentWillMount() {}
566 - render() {
567 - return null;
568 - }
569 - }
570 -
571 - expect(() => {
572 - ReactTestRenderer.create(<AsyncRoot foo={true} />);
573 - }).toWarnDev(
574 - 'Unsafe lifecycle methods were found within a strict-mode tree:' +
575 - '\n in AsyncRoot (at **)' +
576 - '\n\ncomponentWillMount: Please update the following components ' +
577 - 'to use componentDidMount instead: Bar' +
578 - '\n\nLearn more about this warning here:' +
579 - '\nhttps://fb.me/react-strict-mode-warnings',
580 - );
581 -
582 - expect(caughtError).not.toBe(null);
583 - });
584 -
534 it('should also warn inside of "strict" mode trees', () => {
535 const {StrictMode} = React;
536
packages/react/src/__tests__/ReactTypeScriptClass-test.ts
+19 -10
@@ -325,10 +325,13 @@ describe('ReactTypeScriptClass', function() {
325 expect(() =>
326 ReactDOM.render(React.createElement(Empty), container)
327 ).toThrow()
328 - ).toWarnDev(
328 + ).toWarnDev([
329 + // A failed component renders twice in DEV
330 'Warning: Empty(...): No `render` method found on the returned ' +
330 - 'component instance: you may have forgotten to define `render`.'
331 - );
331 + 'component instance: you may have forgotten to define `render`.',
332 + 'Warning: Empty(...): No `render` method found on the returned ' +
333 + 'component instance: you may have forgotten to define `render`.',
334 + ]);
335 });
336
337 it('renders a simple stateless component with prop', function() {
@@ -367,10 +370,12 @@ describe('ReactTypeScriptClass', function() {
370 };
371 }
372 render() {
370 - return React.createElement('div', {className: `${this.state.foo} ${this.state.bar}`});
373 + return React.createElement('div', {
374 + className: `${this.state.foo} ${this.state.bar}`,
375 + });
376 }
377 }
373 - test(React.createElement(Foo, {foo: "foo"}), 'DIV', 'foo bar');
378 + test(React.createElement(Foo, {foo: 'foo'}), 'DIV', 'foo bar');
379 });
380
381 it('warns if state not initialized before static getDerivedStateFromProps', function() {
@@ -382,11 +387,13 @@ describe('ReactTypeScriptClass', function() {
387 };
388 }
389 render() {
385 - return React.createElement('div', {className: `${this.state.foo} ${this.state.bar}`});
390 + return React.createElement('div', {
391 + className: `${this.state.foo} ${this.state.bar}`,
392 + });
393 }
394 }
395 expect(function() {
389 - ReactDOM.render(React.createElement(Foo, {foo: "foo"}), container);
396 + ReactDOM.render(React.createElement(Foo, {foo: 'foo'}), container);
397 }).toWarnDev(
398 'Foo: Did not properly initialize state during construction. ' +
399 'Expected state to be an object, but it was undefined.'
@@ -405,7 +412,9 @@ describe('ReactTypeScriptClass', function() {
412 };
413 }
414 render() {
408 - return React.createElement('div', {className: `${this.state.foo} ${this.state.bar}`});
415 + return React.createElement('div', {
416 + className: `${this.state.foo} ${this.state.bar}`,
417 + });
418 }
419 }
420 test(React.createElement(Foo), 'DIV', 'not-foo bar');
@@ -428,8 +437,8 @@ describe('ReactTypeScriptClass', function() {
437 return React.createElement('div', {className: this.state.value});
438 }
439 }
431 - test(React.createElement(Foo, {update:false}), 'DIV', 'initial');
432 - test(React.createElement(Foo, {update:true}), 'DIV', 'updated');
440 + test(React.createElement(Foo, {update: false}), 'DIV', 'initial');
441 + test(React.createElement(Foo, {update: true}), 'DIV', 'updated');
442 });
443
444 it('renders based on context in the constructor', function() {
packages/shared/ReactErrorUtils.js
+1 -1
@@ -34,7 +34,7 @@ const ReactErrorUtils = {
34 */
35 invokeGuardedCallback: function<A, B, C, D, E, F, Context>(
36 name: string | null,
37 - func: (a: A, b: B, c: C, d: D, e: E, f: F) => void,
37 + func: (a: A, b: B, c: C, d: D, e: E, f: F) => mixed,
38 context: Context,
39 a: A,
40 b: B,
packages/shared/ReactFeatureFlags.js
+8 -1
@@ -10,7 +10,7 @@
10 import invariant from 'fbjs/lib/invariant';
11
12 // Exports ReactDOM.createRoot
13 -export const enableCreateRoot = false;
13 +export const enableCreateRoot = true;
14 export const enableUserTimingAPI = __DEV__;
15
16 // Mutating mode (React DOM, React ART, React Native):
@@ -19,6 +19,9 @@ export const enableMutatingReconciler = true;
19 export const enableNoopReconciler = false;
20 // Experimental persistent mode (Fabric):
21 export const enablePersistentReconciler = false;
22 +// Experimental error-boundary API that can recover from errors within a single
23 +// render phase
24 +export const enableGetDerivedStateFromCatch = false;
25 // Helps identify side effects in begin-phase lifecycle hooks and setState reducers:
26 export const debugRenderPhaseSideEffects = false;
27
@@ -28,6 +31,10 @@ export const debugRenderPhaseSideEffects = false;
31 // This feature flag can be used to control the behavior:
32 export const debugRenderPhaseSideEffectsForStrictMode = __DEV__;
33
34 +// To preserve the "Pause on caught exceptions" behavior of the debugger, we
35 +// replay the begin phase of a failed component inside invokeGuardedCallback.
36 +export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
37 +
38 // Warn about deprecated, async-unsafe lifecycles; relates to RFC #6:
39 export const warnAboutDeprecatedLifecycles = false;
40
packages/shared/ReactTypeOfSideEffect.js
+18 -11
@@ -9,16 +9,23 @@
9
10 export type TypeOfSideEffect = number;
11
12 -// Don't change these two values:
13 -export const NoEffect = 0b00000000;
14 -export const PerformedWork = 0b00000001;
12 +// Don't change these two values. They're used by React Dev Tools.
13 +export const NoEffect = /* */ 0b00000000000;
14 +export const PerformedWork = /* */ 0b00000000001;
15
16 // You can change the rest (and add more).
17 -export const Placement = 0b00000010;
18 -export const Update = 0b00000100;
19 -export const PlacementAndUpdate = 0b00000110;
20 -export const Deletion = 0b00001000;
21 -export const ContentReset = 0b00010000;
22 -export const Callback = 0b00100000;
23 -export const Err = 0b01000000;
24 -export const Ref = 0b10000000;
17 +export const Placement = /* */ 0b00000000010;
18 +export const Update = /* */ 0b00000000100;
19 +export const PlacementAndUpdate = /* */ 0b00000000110;
20 +export const Deletion = /* */ 0b00000001000;
21 +export const ContentReset = /* */ 0b00000010000;
22 +export const Callback = /* */ 0b00000100000;
23 +export const DidCapture = /* */ 0b00001000000;
24 +export const Ref = /* */ 0b00010000000;
25 +export const ErrLog = /* */ 0b00100000000;
26 +
27 +// Union of all host effects
28 +export const HostEffectMask = /* */ 0b00111111111;
29 +
30 +export const Incomplete = /* */ 0b01000000000;
31 +export const ShouldCapture = /* */ 0b10000000000;
packages/shared/forks/ReactFeatureFlags.native-fabric.js
+2
@@ -16,7 +16,9 @@ export const debugRenderPhaseSideEffects = false;
16 export const debugRenderPhaseSideEffectsForStrictMode = false;
17 export const enableCreateRoot = false;
18 export const enableUserTimingAPI = __DEV__;
19 +export const enableGetDerivedStateFromCatch = false;
20 export const warnAboutDeprecatedLifecycles = false;
21 +export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
22
23 // React Fabric uses persistent reconciler.
24 export const enableMutatingReconciler = false;
packages/shared/forks/ReactFeatureFlags.native.js
+2
@@ -14,9 +14,11 @@ import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.native';
14
15 // Re-export dynamic flags from the fbsource version.
16 export const {
17 + enableGetDerivedStateFromCatch,
18 debugRenderPhaseSideEffects,
19 debugRenderPhaseSideEffectsForStrictMode,
20 warnAboutDeprecatedLifecycles,
21 + replayFailedUnitOfWorkWithInvokeGuardedCallback,
22 } = require('ReactFeatureFlags');
23
24 // The rest of the flags are static for better dead code elimination.
packages/shared/forks/ReactFeatureFlags.persistent.js
+2
@@ -16,7 +16,9 @@ export const debugRenderPhaseSideEffects = false;
16 export const debugRenderPhaseSideEffectsForStrictMode = false;
17 export const enableCreateRoot = false;
18 export const enableUserTimingAPI = __DEV__;
19 +export const enableGetDerivedStateFromCatch = false;
20 export const warnAboutDeprecatedLifecycles = false;
21 +export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
22
23 // react-reconciler/persistent entry point
24 // uses a persistent reconciler.
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -12,9 +12,11 @@ import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.www';
12
13 // Re-export dynamic flags from the www version.
14 export const {
15 + enableGetDerivedStateFromCatch,
16 debugRenderPhaseSideEffects,
17 debugRenderPhaseSideEffectsForStrictMode,
18 warnAboutDeprecatedLifecycles,
19 + replayFailedUnitOfWorkWithInvokeGuardedCallback,
20 } = require('ReactFeatureFlags');
21
22 // The rest of the flags are static for better dead code elimination.
packages/shared/invokeGuardedCallback.js
+2 -2
@@ -11,7 +11,7 @@ import invariant from 'fbjs/lib/invariant';
11
12 let invokeGuardedCallback = function<A, B, C, D, E, F, Context>(
13 name: string | null,
14 - func: (a: A, b: B, c: C, d: D, e: E, f: F) => void,
14 + func: (a: A, b: B, c: C, d: D, e: E, f: F) => mixed,
15 context: Context,
16 a: A,
17 b: B,
@@ -63,7 +63,7 @@ if (__DEV__) {
63
64 const invokeGuardedCallbackDev = function<A, B, C, D, E, F, Context>(
65 name: string | null,
66 - func: (a: A, b: B, c: C, d: D, e: E, f: F) => void,
66 + func: (a: A, b: B, c: C, d: D, e: E, f: F) => mixed,
67 context: Context,
68 a: A,
69 b: B,