Skip to content

Commit 06b7db4

Browse files
committed
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.
1 parent e759728 commit 06b7db4

13 files changed

Lines changed: 234 additions & 99 deletions

packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -788,7 +788,7 @@ describe('ReactErrorBoundaries', () => {
788788
]);
789789
});
790790

791-
xit('propagates errors on retry on mounting', () => {
791+
it('propagates errors on retry on mounting', () => {
792792
const container = document.createElement('div');
793793
ReactDOM.render(
794794
<ErrorBoundary>

packages/react-dom/src/__tests__/ReactUpdates-test.js

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,28 +1336,32 @@ describe('ReactUpdates', () => {
13361336
});
13371337

13381338
it('does not fall into an infinite error loop', () => {
1339-
class BadMount extends React.Component {
1340-
componentDidMount() {
1341-
throw new Error('error');
1339+
function BadRender() {
1340+
throw new Error('error');
1341+
}
1342+
1343+
class ErrorBoundary extends React.Component {
1344+
componentDidCatch() {
1345+
this.props.parent.remount();
13421346
}
13431347
render() {
1344-
return null;
1348+
return <BadRender />;
13451349
}
13461350
}
13471351

1348-
class ErrorBoundary extends React.Component {
1349-
componentDidCatch() {
1350-
// Noop
1351-
this.setState({});
1352+
class NonTerminating extends React.Component {
1353+
state = {step: 0};
1354+
remount() {
1355+
this.setState(state => ({step: state.step + 1}));
13521356
}
13531357
render() {
1354-
return <BadMount />;
1358+
return <ErrorBoundary key={this.state.step} parent={this} />;
13551359
}
13561360
}
13571361

13581362
const container = document.createElement('div');
13591363
expect(() => {
1360-
ReactDOM.render(<ErrorBoundary />, container);
1364+
ReactDOM.render(<NonTerminating />, container);
13611365
}).toThrow('Maximum');
13621366
});
13631367
});

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
} from 'shared/ReactTypeOfSideEffect';
4141
import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState';
4242
import {
43+
enableGetDerivedStateFromCatch,
4344
debugRenderPhaseSideEffects,
4445
debugRenderPhaseSideEffectsForStrictMode,
4546
} from 'shared/ReactFeatureFlags';
@@ -49,9 +50,7 @@ import warning from 'fbjs/lib/warning';
4950
import ReactDebugCurrentFiber from './ReactDebugCurrentFiber';
5051
import {cancelWorkTimer} from './ReactDebugFiberPerf';
5152

52-
import ReactFiberClassComponent, {
53-
callGetDerivedStateFromCatch,
54-
} from './ReactFiberClassComponent';
53+
import ReactFiberClassComponent from './ReactFiberClassComponent';
5554
import {
5655
mountChildFibers,
5756
reconcileChildFibers,
@@ -256,19 +255,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
256255
const hasContext = pushLegacyContextProvider(workInProgress);
257256
const instance = workInProgress.stateNode;
258257

259-
// Check if this component captured any errors. If so, grab them off
260-
// the instance.
261-
let didCaptureError = false;
262-
let updateQueue = workInProgress.updateQueue;
263-
if (updateQueue !== null) {
264-
const capturedErrors = updateQueue.capturedValues;
265-
if (capturedErrors !== null) {
266-
didCaptureError = true;
267-
invariant(instance !== null, 'Expected class to have an instance.');
268-
callGetDerivedStateFromCatch(instance, capturedErrors);
269-
}
270-
}
271-
272258
let shouldUpdate;
273259
if (current === null) {
274260
if (instance === null) {
@@ -295,7 +281,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
295281
// We processed the update queue inside updateClassInstance. It may have
296282
// included some errors that were dispatched during the commit phase.
297283
// TODO: Refactor class components so this is less awkward.
298-
updateQueue = workInProgress.updateQueue;
284+
let didCaptureError = false;
285+
const updateQueue = workInProgress.updateQueue;
299286
if (updateQueue !== null && updateQueue.capturedValues !== null) {
300287
workInProgress.effectTag |= DidCapture;
301288
shouldUpdate = true;
@@ -339,8 +326,14 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
339326
let nextChildren;
340327
if (
341328
didCaptureError &&
342-
typeof ctor.getDerivedStateFromCatch !== 'function'
329+
(!enableGetDerivedStateFromCatch ||
330+
typeof ctor.getDerivedStateFromCatch !== 'function')
343331
) {
332+
// If we captured an error, but getDerivedStateFrom catch is not defined,
333+
// unmount all the children. componentDidCatch will schedule an update to
334+
// re-render a fallback. This is temporary until we migrate everyone to
335+
// the new API.
336+
// TODO: Warn in a future release.
344337
nextChildren = null;
345338
} else {
346339
if (__DEV__) {

packages/react-reconciler/src/ReactFiberClassComponent.js

Lines changed: 80 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99

1010
import type {Fiber} from './ReactFiber';
1111
import type {ExpirationTime} from './ReactFiberExpirationTime';
12+
import type {CapturedValue} from './ReactCapturedValue';
1213

1314
import {Update} from 'shared/ReactTypeOfSideEffect';
1415
import {
16+
enableGetDerivedStateFromCatch,
1517
debugRenderPhaseSideEffects,
1618
debugRenderPhaseSideEffectsForStrictMode,
1719
warnAboutDeprecatedLifecycles,
@@ -94,17 +96,17 @@ if (__DEV__) {
9496
});
9597
Object.freeze(fakeInternalInstance);
9698
}
97-
98-
export function callGetDerivedStateFromCatch(
99-
ctor: any,
100-
capturedValues: Array<mixed>,
101-
) {
102-
// TODO:
103-
// for (let i = 0; i < capturedValues.length; i++) {
104-
// const capturedValue: CapturedValue<mixed> = (capturedValues[i]: any);
105-
// const error = capturedValue.value;
106-
// ctor.getDerivedStateFromCatch(error);
107-
// }
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;
108110
}
109111

110112
export default function(
@@ -687,6 +689,8 @@ export default function(
687689
// ever the previously attempted to render - not the "current". However,
688690
// during componentDidUpdate we pass the "current" props.
689691

692+
// In order to support react-lifecycles-compat polyfilled components,
693+
// Unsafe lifecycles should not be invoked for any component with the new gDSFP.
690694
if (
691695
(typeof instance.UNSAFE_componentWillReceiveProps === 'function' ||
692696
typeof instance.componentWillReceiveProps === 'function') &&
@@ -702,10 +706,20 @@ export default function(
702706
}
703707
}
704708

709+
let derivedStateFromProps;
710+
if (oldProps !== newProps) {
711+
derivedStateFromProps = callGetDerivedStateFromProps(
712+
workInProgress,
713+
instance,
714+
newProps,
715+
);
716+
}
717+
705718
// Compute the next state using the memoized state and the update queue.
706719
const oldState = workInProgress.memoizedState;
707720
// TODO: Previous state can be null.
708721
let newState;
722+
let derivedStateFromCatch;
709723
if (workInProgress.updateQueue !== null) {
710724
newState = processUpdateQueue(
711725
null,
@@ -720,27 +734,42 @@ export default function(
720734
if (
721735
updateQueue !== null &&
722736
updateQueue.capturedValues !== null &&
723-
typeof ctor.getDerivedStateFromCatch === 'function'
737+
(enableGetDerivedStateFromCatch &&
738+
typeof ctor.getDerivedStateFromCatch === 'function')
724739
) {
725740
const capturedValues = updateQueue.capturedValues;
726741
// Don't remove these from the update queue yet. We need them in
727742
// finishClassComponent. Do the reset there.
728743
// TODO: This is awkward. Refactor class components.
729744
// updateQueue.capturedValues = null;
730-
callGetDerivedStateFromCatch(ctor, capturedValues);
731-
newState = processUpdateQueue(
732-
null,
733-
workInProgress,
734-
updateQueue,
735-
instance,
736-
newProps,
737-
renderExpirationTime,
745+
derivedStateFromCatch = callGetDerivedStateFromCatch(
746+
ctor,
747+
capturedValues,
738748
);
739749
}
740750
} else {
741751
newState = oldState;
742752
}
743753

754+
if (derivedStateFromProps !== null && derivedStateFromProps !== undefined) {
755+
// Render-phase updates (like this) should not be added to the update queue,
756+
// So that multiple render passes do not enqueue multiple updates.
757+
// Instead, just synchronously merge the returned state into the instance.
758+
newState =
759+
newState === null || newState === undefined
760+
? derivedStateFromProps
761+
: Object.assign({}, newState, derivedStateFromProps);
762+
}
763+
if (derivedStateFromCatch !== null && derivedStateFromCatch !== undefined) {
764+
// Render-phase updates (like this) should not be added to the update queue,
765+
// So that multiple render passes do not enqueue multiple updates.
766+
// Instead, just synchronously merge the returned state into the instance.
767+
newState =
768+
newState === null || newState === undefined
769+
? derivedStateFromCatch
770+
: Object.assign({}, newState, derivedStateFromCatch);
771+
}
772+
744773
if (
745774
oldProps === newProps &&
746775
oldState === newState &&
@@ -752,7 +781,7 @@ export default function(
752781
) {
753782
// If an update was already in progress, we should schedule an Update
754783
// effect even though we're bailing out, so that cWU/cDU are called.
755-
if (typeof instance.componentDidUpdate === 'function') {
784+
if (typeof instance.componentDidMount === 'function') {
756785
workInProgress.effectTag |= Update;
757786
}
758787
return false;
@@ -768,45 +797,23 @@ export default function(
768797
);
769798

770799
if (shouldUpdate) {
771-
if (__DEV__) {
772-
if (workInProgress.mode & StrictMode) {
773-
ReactStrictModeWarnings.recordUnsafeLifecycleWarnings(
774-
workInProgress,
775-
instance,
776-
);
777-
}
778-
779-
if (warnAboutDeprecatedLifecycles) {
780-
ReactStrictModeWarnings.recordDeprecationWarnings(
781-
workInProgress,
782-
instance,
783-
);
784-
}
785-
}
786-
787800
// In order to support react-lifecycles-compat polyfilled components,
788801
// Unsafe lifecycles should not be invoked for any component with the new gDSFP.
789802
if (
790-
(typeof instance.UNSAFE_componentWillMount === 'function' ||
791-
typeof instance.componentWillMount === 'function') &&
803+
(typeof instance.UNSAFE_componentWillUpdate === 'function' ||
804+
typeof instance.componentWillUpdate === 'function') &&
792805
typeof ctor.getDerivedStateFromProps !== 'function'
793806
) {
794-
callComponentWillMount(workInProgress, instance);
795-
// If we had additional state updates during this life-cycle, let's
796-
// process them now.
797-
const updateQueue = workInProgress.updateQueue;
798-
if (updateQueue !== null) {
799-
instance.state = processUpdateQueue(
800-
workInProgress.alternate,
801-
workInProgress,
802-
updateQueue,
803-
instance,
804-
newProps,
805-
renderExpirationTime,
806-
);
807+
startPhaseTimer(workInProgress, 'componentWillUpdate');
808+
if (typeof instance.componentWillUpdate === 'function') {
809+
instance.componentWillUpdate(newProps, newState, newContext);
810+
}
811+
if (typeof instance.UNSAFE_componentWillUpdate === 'function') {
812+
instance.UNSAFE_componentWillUpdate(newProps, newState, newContext);
807813
}
814+
stopPhaseTimer();
808815
}
809-
if (typeof instance.componentDidMount === 'function') {
816+
if (typeof instance.componentDidUpdate === 'function') {
810817
workInProgress.effectTag |= Update;
811818
}
812819
} else {
@@ -868,9 +875,9 @@ export default function(
868875
}
869876
}
870877

871-
let partialState;
878+
let derivedStateFromProps;
872879
if (oldProps !== newProps) {
873-
partialState = callGetDerivedStateFromProps(
880+
derivedStateFromProps = callGetDerivedStateFromProps(
874881
workInProgress,
875882
instance,
876883
newProps,
@@ -881,6 +888,7 @@ export default function(
881888
const oldState = workInProgress.memoizedState;
882889
// TODO: Previous state can be null.
883890
let newState;
891+
let derivedStateFromCatch;
884892
if (workInProgress.updateQueue !== null) {
885893
newState = processUpdateQueue(
886894
current,
@@ -895,35 +903,40 @@ export default function(
895903
if (
896904
updateQueue !== null &&
897905
updateQueue.capturedValues !== null &&
898-
typeof ctor.getDerivedStateFromCatch === 'function'
906+
(enableGetDerivedStateFromCatch &&
907+
typeof ctor.getDerivedStateFromCatch === 'function')
899908
) {
900909
const capturedValues = updateQueue.capturedValues;
901910
// Don't remove these from the update queue yet. We need them in
902911
// finishClassComponent. Do the reset there.
903912
// TODO: This is awkward. Refactor class components.
904913
// updateQueue.capturedValues = null;
905-
callGetDerivedStateFromCatch(ctor, capturedValues);
906-
newState = processUpdateQueue(
907-
null,
908-
workInProgress,
909-
updateQueue,
910-
instance,
911-
newProps,
912-
renderExpirationTime,
914+
derivedStateFromCatch = callGetDerivedStateFromCatch(
915+
ctor,
916+
capturedValues,
913917
);
914918
}
915919
} else {
916920
newState = oldState;
917921
}
918922

919-
if (partialState !== null && partialState !== undefined) {
923+
if (derivedStateFromProps !== null && derivedStateFromProps !== undefined) {
924+
// Render-phase updates (like this) should not be added to the update queue,
925+
// So that multiple render passes do not enqueue multiple updates.
926+
// Instead, just synchronously merge the returned state into the instance.
927+
newState =
928+
newState === null || newState === undefined
929+
? derivedStateFromProps
930+
: Object.assign({}, newState, derivedStateFromProps);
931+
}
932+
if (derivedStateFromCatch !== null && derivedStateFromCatch !== undefined) {
920933
// Render-phase updates (like this) should not be added to the update queue,
921934
// So that multiple render passes do not enqueue multiple updates.
922935
// Instead, just synchronously merge the returned state into the instance.
923936
newState =
924937
newState === null || newState === undefined
925-
? partialState
926-
: Object.assign({}, newState, partialState);
938+
? derivedStateFromCatch
939+
: Object.assign({}, newState, derivedStateFromCatch);
927940
}
928941

929942
if (

0 commit comments

Comments
 (0)