diff --git a/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js b/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
index 316d5caa9acb9..f85fc1fa13eac 100644
--- a/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
@@ -288,9 +288,11 @@ describe('ReactErrorBoundaries', () => {
componentWillUnmount() {
log.push('BrokenComponentWillMountErrorBoundary componentWillUnmount');
}
- componentDidCatch(error) {
- log.push('BrokenComponentWillMountErrorBoundary componentDidCatch');
- this.setState({error});
+ static getDerivedStateFromError(error) {
+ log.push(
+ 'BrokenComponentWillMountErrorBoundary static getDerivedStateFromError',
+ );
+ return {error};
}
};
@@ -318,9 +320,11 @@ describe('ReactErrorBoundaries', () => {
componentWillUnmount() {
log.push('BrokenComponentDidMountErrorBoundary componentWillUnmount');
}
- componentDidCatch(error) {
- log.push('BrokenComponentDidMountErrorBoundary componentDidCatch');
- this.setState({error});
+ static getDerivedStateFromError(error) {
+ log.push(
+ 'BrokenComponentDidMountErrorBoundary static getDerivedStateFromError',
+ );
+ return {error};
}
};
@@ -347,9 +351,9 @@ describe('ReactErrorBoundaries', () => {
componentWillUnmount() {
log.push('BrokenRenderErrorBoundary componentWillUnmount');
}
- componentDidCatch(error) {
- log.push('BrokenRenderErrorBoundary componentDidCatch');
- this.setState({error});
+ static getDerivedStateFromError(error) {
+ log.push('BrokenRenderErrorBoundary static getDerivedStateFromError');
+ return {error};
}
};
@@ -400,8 +404,8 @@ describe('ReactErrorBoundaries', () => {
componentWillUnmount() {
log.push('NoopErrorBoundary componentWillUnmount');
}
- componentDidCatch() {
- log.push('NoopErrorBoundary componentDidCatch');
+ static getDerivedStateFromError() {
+ log.push('NoopErrorBoundary static getDerivedStateFromError');
}
};
@@ -451,9 +455,9 @@ describe('ReactErrorBoundaries', () => {
log.push(`${this.props.logName} render success`);
return
{this.props.children}
;
}
- componentDidCatch(error) {
- log.push(`${this.props.logName} componentDidCatch`);
- this.setState({error});
+ static getDerivedStateFromError(error) {
+ log.push('ErrorBoundary static getDerivedStateFromError');
+ return {error};
}
UNSAFE_componentWillMount() {
log.push(`${this.props.logName} componentWillMount`);
@@ -503,10 +507,10 @@ describe('ReactErrorBoundaries', () => {
componentWillUnmount() {
log.push('RetryErrorBoundary componentWillUnmount');
}
- componentDidCatch(error) {
- log.push('RetryErrorBoundary componentDidCatch [!]');
+ static getDerivedStateFromError(error) {
+ log.push('RetryErrorBoundary static getDerivedStateFromError [!]');
// In Fiber, calling setState() (and failing) is treated as a rethrow.
- this.setState({});
+ return {};
}
};
@@ -629,13 +633,11 @@ describe('ReactErrorBoundaries', () => {
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
- // Fiber mounts with null children before capturing error
- 'ErrorBoundary componentDidMount',
// Catch and render an error message
- 'ErrorBoundary componentDidCatch',
- 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary static getDerivedStateFromError',
+ 'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
- 'ErrorBoundary componentDidUpdate',
+ 'ErrorBoundary componentDidMount',
]);
log.length = 0;
@@ -657,13 +659,11 @@ describe('ReactErrorBoundaries', () => {
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenConstructor constructor [!]',
- // Fiber mounts with null children before capturing error
- 'ErrorBoundary componentDidMount',
// Catch and render an error message
- 'ErrorBoundary componentDidCatch',
- 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary static getDerivedStateFromError',
+ 'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
- 'ErrorBoundary componentDidUpdate',
+ 'ErrorBoundary componentDidMount',
]);
log.length = 0;
@@ -686,11 +686,11 @@ describe('ReactErrorBoundaries', () => {
'ErrorBoundary render success',
'BrokenComponentWillMount constructor',
'BrokenComponentWillMount componentWillMount [!]',
- 'ErrorBoundary componentDidMount',
- 'ErrorBoundary componentDidCatch',
- 'ErrorBoundary componentWillUpdate',
+ // Catch and render an error message
+ 'ErrorBoundary static getDerivedStateFromError',
+ 'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
- 'ErrorBoundary componentDidUpdate',
+ 'ErrorBoundary componentDidMount',
]);
log.length = 0;
@@ -769,15 +769,14 @@ describe('ReactErrorBoundaries', () => {
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
- 'ErrorBoundary componentDidMount',
- 'ErrorBoundary componentDidCatch',
- 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary static getDerivedStateFromError',
+ 'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorMessage constructor',
'ErrorMessage componentWillMount',
'ErrorMessage render',
'ErrorMessage componentDidMount',
- 'ErrorBoundary componentDidUpdate',
+ 'ErrorBoundary componentDidMount',
]);
log.length = 0;
@@ -809,22 +808,18 @@ describe('ReactErrorBoundaries', () => {
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
- // In Fiber, failed error boundaries render null before attempting to recover
- 'RetryErrorBoundary componentDidMount',
- 'RetryErrorBoundary componentDidCatch [!]',
- 'ErrorBoundary componentDidMount',
// Retry
+ 'RetryErrorBoundary static getDerivedStateFromError [!]',
+ 'RetryErrorBoundary componentWillMount',
'RetryErrorBoundary render',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
// This time, the error propagates to the higher boundary
- 'RetryErrorBoundary componentWillUnmount',
- 'ErrorBoundary componentDidCatch',
- // Render the error
- 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary static getDerivedStateFromError',
+ 'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
- 'ErrorBoundary componentDidUpdate',
+ 'ErrorBoundary componentDidMount',
]);
log.length = 0;
@@ -848,11 +843,10 @@ describe('ReactErrorBoundaries', () => {
'BrokenComponentWillMountErrorBoundary constructor',
'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
// The error propagates to the higher boundary
- 'ErrorBoundary componentDidMount',
- 'ErrorBoundary componentDidCatch',
- 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary static getDerivedStateFromError',
+ 'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
- 'ErrorBoundary componentDidUpdate',
+ 'ErrorBoundary componentDidMount',
]);
log.length = 0;
@@ -881,21 +875,15 @@ describe('ReactErrorBoundaries', () => {
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
- // The first error boundary catches the error
- // It adjusts state but throws displaying the message
- // Finish mounting with null children
- 'BrokenRenderErrorBoundary componentDidMount',
// Attempt to handle the error
- 'BrokenRenderErrorBoundary componentDidCatch',
- 'ErrorBoundary componentDidMount',
+ 'BrokenRenderErrorBoundary static getDerivedStateFromError',
+ 'BrokenRenderErrorBoundary componentWillMount',
'BrokenRenderErrorBoundary render error [!]',
- // Boundary fails with new error, propagate to next boundary
- 'BrokenRenderErrorBoundary componentWillUnmount',
// Attempt to handle the error again
- 'ErrorBoundary componentDidCatch',
- 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary static getDerivedStateFromError',
+ 'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
- 'ErrorBoundary componentDidUpdate',
+ 'ErrorBoundary componentDidMount',
]);
log.length = 0;
@@ -930,14 +918,11 @@ describe('ReactErrorBoundaries', () => {
'Normal constructor',
'Normal componentWillMount',
'Normal render',
- // Finish mounting with null children
- 'ErrorBoundary componentDidMount',
// Handle the error
- 'ErrorBoundary componentDidCatch',
- // Render the error message
- 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary static getDerivedStateFromError',
+ 'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
- 'ErrorBoundary componentDidUpdate',
+ 'ErrorBoundary componentDidMount',
]);
log.length = 0;
@@ -969,16 +954,12 @@ describe('ReactErrorBoundaries', () => {
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
- // Handle error:
- // Finish mounting with null children
- 'ErrorBoundary componentDidMount',
// Handle the error
- 'ErrorBoundary componentDidCatch',
- // Render the error message
- 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary static getDerivedStateFromError',
+ 'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'Error message ref is set to [object HTMLDivElement]',
- 'ErrorBoundary componentDidUpdate',
+ 'ErrorBoundary componentDidMount',
]);
log.length = 0;
@@ -1009,15 +990,11 @@ describe('ReactErrorBoundaries', () => {
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
- // Handle error:
- // Finish mounting with null children
- 'ErrorBoundary componentDidMount',
// Handle the error
- 'ErrorBoundary componentDidCatch',
- // Render the error message
- 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary static getDerivedStateFromError',
+ 'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
- 'ErrorBoundary componentDidUpdate',
+ 'ErrorBoundary componentDidMount',
]);
expect(errorMessageRef.current.toString()).toEqual(
'[object HTMLDivElement]',
@@ -1058,7 +1035,6 @@ describe('ReactErrorBoundaries', () => {
,
container,
);
-
log.length = 0;
ReactDOM.render(
@@ -1082,14 +1058,12 @@ describe('ReactErrorBoundaries', () => {
'Normal2 render',
// BrokenConstructor will abort rendering:
'BrokenConstructor constructor [!]',
- // Finish updating with null children
- 'Normal componentWillUnmount',
- 'ErrorBoundary componentDidUpdate',
// Handle the error
- 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary static getDerivedStateFromError',
// Render the error message
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
+ 'Normal componentWillUnmount',
'ErrorBoundary componentDidUpdate',
]);
@@ -1131,14 +1105,12 @@ describe('ReactErrorBoundaries', () => {
// BrokenComponentWillMount will abort rendering:
'BrokenComponentWillMount constructor',
'BrokenComponentWillMount componentWillMount [!]',
- // Finish updating with null children
- 'Normal componentWillUnmount',
- 'ErrorBoundary componentDidUpdate',
// Handle the error
- 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary static getDerivedStateFromError',
// Render the error message
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
+ 'Normal componentWillUnmount',
'ErrorBoundary componentDidUpdate',
]);
@@ -1175,14 +1147,13 @@ describe('ReactErrorBoundaries', () => {
'Normal render',
// BrokenComponentWillReceiveProps will abort rendering:
'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
- // Finish updating with null children
- 'Normal componentWillUnmount',
- 'BrokenComponentWillReceiveProps componentWillUnmount',
- 'ErrorBoundary componentDidUpdate',
// Handle the error
- 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary static getDerivedStateFromError',
+ // Render the error message
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
+ 'Normal componentWillUnmount',
+ 'BrokenComponentWillReceiveProps componentWillUnmount',
'ErrorBoundary componentDidUpdate',
]);
@@ -1220,14 +1191,12 @@ describe('ReactErrorBoundaries', () => {
// BrokenComponentWillUpdate will abort rendering:
'BrokenComponentWillUpdate componentWillReceiveProps',
'BrokenComponentWillUpdate componentWillUpdate [!]',
- // Finish updating with null children
- 'Normal componentWillUnmount',
- 'BrokenComponentWillUpdate componentWillUnmount',
- 'ErrorBoundary componentDidUpdate',
// Handle the error
- 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
+ 'Normal componentWillUnmount',
+ 'BrokenComponentWillUpdate componentWillUnmount',
'ErrorBoundary componentDidUpdate',
]);
@@ -1270,13 +1239,11 @@ describe('ReactErrorBoundaries', () => {
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
- // Finish updating with null children
- 'Normal componentWillUnmount',
- 'ErrorBoundary componentDidUpdate',
// Handle the error
- 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
+ 'Normal componentWillUnmount',
'ErrorBoundary componentDidUpdate',
]);
@@ -1329,15 +1296,14 @@ describe('ReactErrorBoundaries', () => {
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
- // Finish updating with null children
- 'Child1 ref is set to null',
- 'ErrorBoundary componentDidUpdate',
// Handle the error
- 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
- 'Error message ref is set to [object HTMLDivElement]',
+ // Update Child1 ref since Child1 has been unmounted
// Child2 ref is never set because its mounting aborted
+ 'Child1 ref is set to null',
+ 'Error message ref is set to [object HTMLDivElement]',
'ErrorBoundary componentDidUpdate',
]);
@@ -1383,15 +1349,15 @@ describe('ReactErrorBoundaries', () => {
// The components have updated in this phase
'BrokenComponentWillUnmount componentDidUpdate',
'ErrorBoundary componentDidUpdate',
- // Now that commit phase is done, Fiber unmounts the boundary's children
- 'BrokenComponentWillUnmount componentWillUnmount [!]',
- 'ErrorBoundary componentDidCatch',
// The initial render was aborted, so
// Fiber retries from the root.
+ 'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
'ErrorBoundary componentDidUpdate',
// The second willUnmount error should be captured and logged, too.
- 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
// Render an error now (stack will do it later)
'ErrorBoundary render error',
@@ -1444,16 +1410,15 @@ describe('ReactErrorBoundaries', () => {
'BrokenComponentWillUnmount componentDidUpdate',
'Normal componentDidUpdate',
'ErrorBoundary componentDidUpdate',
- 'Normal componentWillUnmount',
- 'BrokenComponentWillUnmount componentWillUnmount [!]',
// Now that commit phase is done, Fiber handles errors
- 'ErrorBoundary componentDidCatch',
- // The initial render was aborted, so
- // Fiber retries from the root.
+ 'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'Normal componentWillUnmount',
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
'ErrorBoundary componentDidUpdate',
// The second willUnmount error should be captured and logged, too.
- 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
// Render an error now (stack will do it later)
'ErrorBoundary render error',
@@ -1512,13 +1477,11 @@ describe('ReactErrorBoundaries', () => {
'InnerErrorBoundary render success',
// Try unmounting child
'BrokenComponentWillUnmount componentWillUnmount [!]',
- // Fiber proceeds with lifecycles despite errors
- // Inner and outer boundaries have updated in this phase
- 'InnerErrorBoundary componentDidUpdate',
- 'OuterErrorBoundary componentDidUpdate',
// Now that commit phase is done, Fiber handles errors
// Only inner boundary receives the error:
- 'InnerErrorBoundary componentDidCatch',
+ 'InnerErrorBoundary componentDidUpdate',
+ 'OuterErrorBoundary componentDidUpdate',
+ 'ErrorBoundary static getDerivedStateFromError',
'InnerErrorBoundary componentWillUpdate',
// Render an error now
'InnerErrorBoundary render error',
@@ -1723,7 +1686,7 @@ describe('ReactErrorBoundaries', () => {
expect(log).toEqual([
'Stateful render [!]',
- 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'ErrorBoundary componentDidUpdate',
@@ -1768,20 +1731,20 @@ describe('ReactErrorBoundaries', () => {
'BrokenComponentDidMount componentDidMount [!]',
// Continue despite the error
'LastChild componentDidMount',
- 'ErrorBoundary componentDidMount',
// Now we are ready to handle the error
+ 'ErrorBoundary componentDidMount',
+ 'ErrorBoundary static getDerivedStateFromError',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
// Safely unmount every child
'BrokenComponentWillUnmount componentWillUnmount [!]',
// Continue unmounting safely despite any errors
'Normal componentWillUnmount',
'BrokenComponentDidMount componentWillUnmount',
'LastChild componentWillUnmount',
- // Handle the error
- 'ErrorBoundary componentDidCatch',
- 'ErrorBoundary componentWillUpdate',
// The willUnmount error should be captured and logged, too.
'ErrorBoundary componentDidUpdate',
- 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
// The update has finished
@@ -1819,11 +1782,11 @@ describe('ReactErrorBoundaries', () => {
// All lifecycles run
'BrokenComponentDidUpdate componentDidUpdate [!]',
'ErrorBoundary componentDidUpdate',
- 'BrokenComponentDidUpdate componentWillUnmount',
// Then, error is handled
- 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
+ 'BrokenComponentDidUpdate componentWillUnmount',
'ErrorBoundary componentDidUpdate',
]);
@@ -1855,12 +1818,12 @@ describe('ReactErrorBoundaries', () => {
'BrokenComponentDidMountErrorBoundary componentDidMount [!]',
// Fiber proceeds with the hooks
'ErrorBoundary componentDidMount',
- 'BrokenComponentDidMountErrorBoundary componentWillUnmount',
// The error propagates to the higher boundary
- 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary static getDerivedStateFromError',
// Fiber retries from the root
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
+ 'BrokenComponentDidMountErrorBoundary componentWillUnmount',
'ErrorBoundary componentDidUpdate',
]);
@@ -1869,7 +1832,7 @@ describe('ReactErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
- it('calls componentDidCatch for each error that is captured', () => {
+ it('calls static getDerivedStateFromError for each error that is captured', () => {
function renderUnmountError(error) {
return Caught an unmounting error: {error.message}.
;
}
@@ -1947,16 +1910,16 @@ describe('ReactErrorBoundaries', () => {
'OuterErrorBoundary componentDidUpdate',
// After the commit phase, attempt to recover from any errors that
// were captured
- 'BrokenComponentDidUpdate componentWillUnmount',
- 'BrokenComponentDidUpdate componentWillUnmount',
- 'InnerUnmountBoundary componentDidCatch',
- 'InnerUnmountBoundary componentDidCatch',
- 'InnerUpdateBoundary componentDidCatch',
- 'InnerUpdateBoundary componentDidCatch',
+ 'ErrorBoundary static getDerivedStateFromError',
+ 'ErrorBoundary static getDerivedStateFromError',
'InnerUnmountBoundary componentWillUpdate',
'InnerUnmountBoundary render error',
+ 'ErrorBoundary static getDerivedStateFromError',
+ 'ErrorBoundary static getDerivedStateFromError',
'InnerUpdateBoundary componentWillUpdate',
'InnerUpdateBoundary render error',
+ 'BrokenComponentDidUpdate componentWillUnmount',
+ 'BrokenComponentDidUpdate componentWillUnmount',
'InnerUnmountBoundary componentDidUpdate',
'InnerUpdateBoundary componentDidUpdate',
]);
@@ -2003,16 +1966,18 @@ describe('ReactErrorBoundaries', () => {
it('renders empty output if error boundary does not handle the error', () => {
const container = document.createElement('div');
- ReactDOM.render(
-
- Sibling
-
-
-
-
,
- container,
- );
- expect(container.firstChild.textContent).toBe('Sibling');
+ expect(() =>
+ ReactDOM.render(
+
+ Sibling
+
+
+
+
,
+ container,
+ ),
+ ).toThrow('Hello');
+ expect(container.innerHTML).toBe('');
expect(log).toEqual([
'NoopErrorBoundary constructor',
'NoopErrorBoundary componentWillMount',
@@ -2020,15 +1985,13 @@ describe('ReactErrorBoundaries', () => {
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
- // In Fiber, noop error boundaries render null
- 'NoopErrorBoundary componentDidMount',
- 'NoopErrorBoundary componentDidCatch',
- // Nothing happens.
+ // Noop error boundaries retry render (and fail again)
+ 'NoopErrorBoundary static getDerivedStateFromError',
+ 'NoopErrorBoundary render',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
]);
-
- log.length = 0;
- ReactDOM.unmountComponentAtNode(container);
- expect(log).toEqual(['NoopErrorBoundary componentWillUnmount']);
});
it('passes first error when two errors happen in commit', () => {
@@ -2121,4 +2084,69 @@ describe('ReactErrorBoundaries', () => {
// Error should be the first thrown
expect(caughtError.message).toBe('child sad');
});
+
+ it('should warn if an error boundary with only componentDidCatch does not update state', () => {
+ class InvalidErrorBoundary extends React.Component {
+ componentDidCatch(error, info) {
+ // This component does not define getDerivedStateFromError().
+ // It also doesn't call setState().
+ // So it would swallow errors (which is probably unintentional).
+ }
+ render() {
+ return this.props.children;
+ }
+ }
+
+ const Throws = () => {
+ throw new Error('expected');
+ };
+
+ const container = document.createElement('div');
+ expect(() => {
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ }).toWarnDev(
+ 'InvalidErrorBoundary: Error boundaries should implement getDerivedStateFromError(). ' +
+ 'In that method, return a state update to display an error message or fallback UI.',
+ {withoutStack: true},
+ );
+ expect(container.textContent).toBe('');
+ });
+
+ it('should call both componentDidCatch and getDerivedStateFromError if both exist on a component', () => {
+ let componentDidCatchError, getDerivedStateFromErrorError;
+ class ErrorBoundaryWithBothMethods extends React.Component {
+ state = {error: null};
+ static getDerivedStateFromError(error) {
+ getDerivedStateFromErrorError = error;
+ return {error};
+ }
+ componentDidCatch(error, info) {
+ componentDidCatchError = error;
+ }
+ render() {
+ return this.state.error ? 'ErrorBoundary' : this.props.children;
+ }
+ }
+
+ const thrownError = new Error('expected');
+ const Throws = () => {
+ throw thrownError;
+ };
+
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('ErrorBoundary');
+ expect(componentDidCatchError).toBe(thrownError);
+ expect(getDerivedStateFromErrorError).toBe(thrownError);
+ });
});
diff --git a/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js b/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js
new file mode 100644
index 0000000000000..7cec1ac6e96fa
--- /dev/null
+++ b/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js
@@ -0,0 +1,2130 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+let PropTypes;
+let React;
+let ReactDOM;
+let ReactFeatureFlags;
+
+// TODO: Refactor this test once componentDidCatch setState is deprecated.
+describe('ReactLegacyErrorBoundaries', () => {
+ let log;
+
+ let BrokenConstructor;
+ let BrokenComponentWillMount;
+ let BrokenComponentDidMount;
+ let BrokenComponentWillReceiveProps;
+ let BrokenComponentWillUpdate;
+ let BrokenComponentDidUpdate;
+ let BrokenComponentWillUnmount;
+ let BrokenRenderErrorBoundary;
+ let BrokenComponentWillMountErrorBoundary;
+ let BrokenComponentDidMountErrorBoundary;
+ let BrokenRender;
+ let ErrorBoundary;
+ let ErrorMessage;
+ let NoopErrorBoundary;
+ let RetryErrorBoundary;
+ let Normal;
+
+ beforeEach(() => {
+ jest.resetModules();
+ PropTypes = require('prop-types');
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
+ ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
+ ReactDOM = require('react-dom');
+ React = require('react');
+
+ log = [];
+
+ BrokenConstructor = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('BrokenConstructor constructor [!]');
+ throw new Error('Hello');
+ }
+ render() {
+ log.push('BrokenConstructor render');
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('BrokenConstructor componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenConstructor componentDidMount');
+ }
+ UNSAFE_componentWillReceiveProps() {
+ log.push('BrokenConstructor componentWillReceiveProps');
+ }
+ UNSAFE_componentWillUpdate() {
+ log.push('BrokenConstructor componentWillUpdate');
+ }
+ componentDidUpdate() {
+ log.push('BrokenConstructor componentDidUpdate');
+ }
+ componentWillUnmount() {
+ log.push('BrokenConstructor componentWillUnmount');
+ }
+ };
+
+ BrokenComponentWillMount = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('BrokenComponentWillMount constructor');
+ }
+ render() {
+ log.push('BrokenComponentWillMount render');
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('BrokenComponentWillMount componentWillMount [!]');
+ throw new Error('Hello');
+ }
+ componentDidMount() {
+ log.push('BrokenComponentWillMount componentDidMount');
+ }
+ UNSAFE_componentWillReceiveProps() {
+ log.push('BrokenComponentWillMount componentWillReceiveProps');
+ }
+ UNSAFE_componentWillUpdate() {
+ log.push('BrokenComponentWillMount componentWillUpdate');
+ }
+ componentDidUpdate() {
+ log.push('BrokenComponentWillMount componentDidUpdate');
+ }
+ componentWillUnmount() {
+ log.push('BrokenComponentWillMount componentWillUnmount');
+ }
+ };
+
+ BrokenComponentDidMount = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('BrokenComponentDidMount constructor');
+ }
+ render() {
+ log.push('BrokenComponentDidMount render');
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('BrokenComponentDidMount componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenComponentDidMount componentDidMount [!]');
+ throw new Error('Hello');
+ }
+ UNSAFE_componentWillReceiveProps() {
+ log.push('BrokenComponentDidMount componentWillReceiveProps');
+ }
+ UNSAFE_componentWillUpdate() {
+ log.push('BrokenComponentDidMount componentWillUpdate');
+ }
+ componentDidUpdate() {
+ log.push('BrokenComponentDidMount componentDidUpdate');
+ }
+ componentWillUnmount() {
+ log.push('BrokenComponentDidMount componentWillUnmount');
+ }
+ };
+
+ BrokenComponentWillReceiveProps = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('BrokenComponentWillReceiveProps constructor');
+ }
+ render() {
+ log.push('BrokenComponentWillReceiveProps render');
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('BrokenComponentWillReceiveProps componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenComponentWillReceiveProps componentDidMount');
+ }
+ UNSAFE_componentWillReceiveProps() {
+ log.push(
+ 'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
+ );
+ throw new Error('Hello');
+ }
+ UNSAFE_componentWillUpdate() {
+ log.push('BrokenComponentWillReceiveProps componentWillUpdate');
+ }
+ componentDidUpdate() {
+ log.push('BrokenComponentWillReceiveProps componentDidUpdate');
+ }
+ componentWillUnmount() {
+ log.push('BrokenComponentWillReceiveProps componentWillUnmount');
+ }
+ };
+
+ BrokenComponentWillUpdate = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('BrokenComponentWillUpdate constructor');
+ }
+ render() {
+ log.push('BrokenComponentWillUpdate render');
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('BrokenComponentWillUpdate componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenComponentWillUpdate componentDidMount');
+ }
+ UNSAFE_componentWillReceiveProps() {
+ log.push('BrokenComponentWillUpdate componentWillReceiveProps');
+ }
+ UNSAFE_componentWillUpdate() {
+ log.push('BrokenComponentWillUpdate componentWillUpdate [!]');
+ throw new Error('Hello');
+ }
+ componentDidUpdate() {
+ log.push('BrokenComponentWillUpdate componentDidUpdate');
+ }
+ componentWillUnmount() {
+ log.push('BrokenComponentWillUpdate componentWillUnmount');
+ }
+ };
+
+ BrokenComponentDidUpdate = class extends React.Component {
+ static defaultProps = {
+ errorText: 'Hello',
+ };
+ constructor(props) {
+ super(props);
+ log.push('BrokenComponentDidUpdate constructor');
+ }
+ render() {
+ log.push('BrokenComponentDidUpdate render');
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('BrokenComponentDidUpdate componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenComponentDidUpdate componentDidMount');
+ }
+ UNSAFE_componentWillReceiveProps() {
+ log.push('BrokenComponentDidUpdate componentWillReceiveProps');
+ }
+ UNSAFE_componentWillUpdate() {
+ log.push('BrokenComponentDidUpdate componentWillUpdate');
+ }
+ componentDidUpdate() {
+ log.push('BrokenComponentDidUpdate componentDidUpdate [!]');
+ throw new Error(this.props.errorText);
+ }
+ componentWillUnmount() {
+ log.push('BrokenComponentDidUpdate componentWillUnmount');
+ }
+ };
+
+ BrokenComponentWillUnmount = class extends React.Component {
+ static defaultProps = {
+ errorText: 'Hello',
+ };
+ constructor(props) {
+ super(props);
+ log.push('BrokenComponentWillUnmount constructor');
+ }
+ render() {
+ log.push('BrokenComponentWillUnmount render');
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('BrokenComponentWillUnmount componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenComponentWillUnmount componentDidMount');
+ }
+ UNSAFE_componentWillReceiveProps() {
+ log.push('BrokenComponentWillUnmount componentWillReceiveProps');
+ }
+ UNSAFE_componentWillUpdate() {
+ log.push('BrokenComponentWillUnmount componentWillUpdate');
+ }
+ componentDidUpdate() {
+ log.push('BrokenComponentWillUnmount componentDidUpdate');
+ }
+ componentWillUnmount() {
+ log.push('BrokenComponentWillUnmount componentWillUnmount [!]');
+ throw new Error(this.props.errorText);
+ }
+ };
+
+ BrokenComponentWillMountErrorBoundary = class extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {error: null};
+ log.push('BrokenComponentWillMountErrorBoundary constructor');
+ }
+ render() {
+ if (this.state.error) {
+ log.push('BrokenComponentWillMountErrorBoundary render error');
+ return Caught an error: {this.state.error.message}.
;
+ }
+ log.push('BrokenComponentWillMountErrorBoundary render success');
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push(
+ 'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
+ );
+ throw new Error('Hello');
+ }
+ componentDidMount() {
+ log.push('BrokenComponentWillMountErrorBoundary componentDidMount');
+ }
+ componentWillUnmount() {
+ log.push('BrokenComponentWillMountErrorBoundary componentWillUnmount');
+ }
+ componentDidCatch(error) {
+ log.push('BrokenComponentWillMountErrorBoundary componentDidCatch');
+ this.setState({error});
+ }
+ };
+
+ BrokenComponentDidMountErrorBoundary = class extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {error: null};
+ log.push('BrokenComponentDidMountErrorBoundary constructor');
+ }
+ render() {
+ if (this.state.error) {
+ log.push('BrokenComponentDidMountErrorBoundary render error');
+ return Caught an error: {this.state.error.message}.
;
+ }
+ log.push('BrokenComponentDidMountErrorBoundary render success');
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('BrokenComponentDidMountErrorBoundary componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenComponentDidMountErrorBoundary componentDidMount [!]');
+ throw new Error('Hello');
+ }
+ componentWillUnmount() {
+ log.push('BrokenComponentDidMountErrorBoundary componentWillUnmount');
+ }
+ componentDidCatch(error) {
+ log.push('BrokenComponentDidMountErrorBoundary componentDidCatch');
+ this.setState({error});
+ }
+ };
+
+ BrokenRenderErrorBoundary = class extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {error: null};
+ log.push('BrokenRenderErrorBoundary constructor');
+ }
+ render() {
+ if (this.state.error) {
+ log.push('BrokenRenderErrorBoundary render error [!]');
+ throw new Error('Hello');
+ }
+ log.push('BrokenRenderErrorBoundary render success');
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('BrokenRenderErrorBoundary componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenRenderErrorBoundary componentDidMount');
+ }
+ componentWillUnmount() {
+ log.push('BrokenRenderErrorBoundary componentWillUnmount');
+ }
+ componentDidCatch(error) {
+ log.push('BrokenRenderErrorBoundary componentDidCatch');
+ this.setState({error});
+ }
+ };
+
+ BrokenRender = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('BrokenRender constructor');
+ }
+ render() {
+ log.push('BrokenRender render [!]');
+ throw new Error('Hello');
+ }
+ UNSAFE_componentWillMount() {
+ log.push('BrokenRender componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenRender componentDidMount');
+ }
+ UNSAFE_componentWillReceiveProps() {
+ log.push('BrokenRender componentWillReceiveProps');
+ }
+ UNSAFE_componentWillUpdate() {
+ log.push('BrokenRender componentWillUpdate');
+ }
+ componentDidUpdate() {
+ log.push('BrokenRender componentDidUpdate');
+ }
+ componentWillUnmount() {
+ log.push('BrokenRender componentWillUnmount');
+ }
+ };
+
+ NoopErrorBoundary = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('NoopErrorBoundary constructor');
+ }
+ render() {
+ log.push('NoopErrorBoundary render');
+ return ;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('NoopErrorBoundary componentWillMount');
+ }
+ componentDidMount() {
+ log.push('NoopErrorBoundary componentDidMount');
+ }
+ componentWillUnmount() {
+ log.push('NoopErrorBoundary componentWillUnmount');
+ }
+ componentDidCatch() {
+ log.push('NoopErrorBoundary componentDidCatch');
+ }
+ };
+
+ Normal = class extends React.Component {
+ static defaultProps = {
+ logName: 'Normal',
+ };
+ constructor(props) {
+ super(props);
+ log.push(`${this.props.logName} constructor`);
+ }
+ render() {
+ log.push(`${this.props.logName} render`);
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push(`${this.props.logName} componentWillMount`);
+ }
+ componentDidMount() {
+ log.push(`${this.props.logName} componentDidMount`);
+ }
+ UNSAFE_componentWillReceiveProps() {
+ log.push(`${this.props.logName} componentWillReceiveProps`);
+ }
+ UNSAFE_componentWillUpdate() {
+ log.push(`${this.props.logName} componentWillUpdate`);
+ }
+ componentDidUpdate() {
+ log.push(`${this.props.logName} componentDidUpdate`);
+ }
+ componentWillUnmount() {
+ log.push(`${this.props.logName} componentWillUnmount`);
+ }
+ };
+
+ ErrorBoundary = class extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {error: null};
+ log.push(`${this.props.logName} constructor`);
+ }
+ render() {
+ if (this.state.error && !this.props.forceRetry) {
+ log.push(`${this.props.logName} render error`);
+ return this.props.renderError(this.state.error, this.props);
+ }
+ log.push(`${this.props.logName} render success`);
+ return {this.props.children}
;
+ }
+ componentDidCatch(error) {
+ log.push(`${this.props.logName} componentDidCatch`);
+ this.setState({error});
+ }
+ UNSAFE_componentWillMount() {
+ log.push(`${this.props.logName} componentWillMount`);
+ }
+ componentDidMount() {
+ log.push(`${this.props.logName} componentDidMount`);
+ }
+ UNSAFE_componentWillReceiveProps() {
+ log.push(`${this.props.logName} componentWillReceiveProps`);
+ }
+ UNSAFE_componentWillUpdate() {
+ log.push(`${this.props.logName} componentWillUpdate`);
+ }
+ componentDidUpdate() {
+ log.push(`${this.props.logName} componentDidUpdate`);
+ }
+ componentWillUnmount() {
+ log.push(`${this.props.logName} componentWillUnmount`);
+ }
+ };
+ ErrorBoundary.defaultProps = {
+ logName: 'ErrorBoundary',
+ renderError(error, props) {
+ return (
+
+ Caught an error: {error.message}.
+
+ );
+ },
+ };
+
+ RetryErrorBoundary = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('RetryErrorBoundary constructor');
+ }
+ render() {
+ log.push('RetryErrorBoundary render');
+ return ;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('RetryErrorBoundary componentWillMount');
+ }
+ componentDidMount() {
+ log.push('RetryErrorBoundary componentDidMount');
+ }
+ componentWillUnmount() {
+ log.push('RetryErrorBoundary componentWillUnmount');
+ }
+ componentDidCatch(error) {
+ log.push('RetryErrorBoundary componentDidCatch [!]');
+ // In Fiber, calling setState() (and failing) is treated as a rethrow.
+ this.setState({});
+ }
+ };
+
+ ErrorMessage = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('ErrorMessage constructor');
+ }
+ UNSAFE_componentWillMount() {
+ log.push('ErrorMessage componentWillMount');
+ }
+ componentDidMount() {
+ log.push('ErrorMessage componentDidMount');
+ }
+ componentWillUnmount() {
+ log.push('ErrorMessage componentWillUnmount');
+ }
+ render() {
+ log.push('ErrorMessage render');
+ return Caught an error: {this.props.message}.
;
+ }
+ };
+ });
+
+ it('does not swallow exceptions on mounting without boundaries', () => {
+ let container = document.createElement('div');
+ expect(() => {
+ ReactDOM.render(, container);
+ }).toThrow('Hello');
+
+ container = document.createElement('div');
+ expect(() => {
+ ReactDOM.render(, container);
+ }).toThrow('Hello');
+
+ container = document.createElement('div');
+ expect(() => {
+ ReactDOM.render(, container);
+ }).toThrow('Hello');
+ });
+
+ it('does not swallow exceptions on updating without boundaries', () => {
+ let container = document.createElement('div');
+ ReactDOM.render(, container);
+ expect(() => {
+ ReactDOM.render(, container);
+ }).toThrow('Hello');
+
+ container = document.createElement('div');
+ ReactDOM.render(, container);
+ expect(() => {
+ ReactDOM.render(, container);
+ }).toThrow('Hello');
+
+ container = document.createElement('div');
+ ReactDOM.render(, container);
+ expect(() => {
+ ReactDOM.render(, container);
+ }).toThrow('Hello');
+ });
+
+ it('does not swallow exceptions on unmounting without boundaries', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(, container);
+ expect(() => {
+ ReactDOM.unmountComponentAtNode(container);
+ }).toThrow('Hello');
+ });
+
+ it('prevents errors from leaking into other roots', () => {
+ const container1 = document.createElement('div');
+ const container2 = document.createElement('div');
+ const container3 = document.createElement('div');
+
+ ReactDOM.render(Before 1, container1);
+ expect(() => {
+ ReactDOM.render(, container2);
+ }).toThrow('Hello');
+ ReactDOM.render(
+
+
+ ,
+ container3,
+ );
+ expect(container1.firstChild.textContent).toBe('Before 1');
+ expect(container2.firstChild).toBe(null);
+ expect(container3.firstChild.textContent).toBe('Caught an error: Hello.');
+
+ ReactDOM.render(After 1, container1);
+ ReactDOM.render(After 2, container2);
+ ReactDOM.render(
+ After 3,
+ container3,
+ );
+ expect(container1.firstChild.textContent).toBe('After 1');
+ expect(container2.firstChild.textContent).toBe('After 2');
+ expect(container3.firstChild.textContent).toBe('After 3');
+
+ ReactDOM.unmountComponentAtNode(container1);
+ ReactDOM.unmountComponentAtNode(container2);
+ ReactDOM.unmountComponentAtNode(container3);
+ expect(container1.firstChild).toBe(null);
+ expect(container2.firstChild).toBe(null);
+ expect(container3.firstChild).toBe(null);
+ });
+
+ it('renders an error state if child throws in render', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // Fiber mounts with null children before capturing error
+ 'ErrorBoundary componentDidMount',
+ // Catch and render an error message
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('renders an error state if child throws in constructor', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenConstructor constructor [!]',
+ // Fiber mounts with null children before capturing error
+ 'ErrorBoundary componentDidMount',
+ // Catch and render an error message
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('renders an error state if child throws in componentWillMount', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenComponentWillMount constructor',
+ 'BrokenComponentWillMount componentWillMount [!]',
+ 'ErrorBoundary componentDidMount',
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('renders an error state if context provider throws in componentWillMount', () => {
+ class BrokenComponentWillMountWithContext extends React.Component {
+ static childContextTypes = {foo: PropTypes.number};
+ getChildContext() {
+ return {foo: 42};
+ }
+ render() {
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ throw new Error('Hello');
+ }
+ }
+
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ });
+
+ it('renders an error state if module-style context provider throws in componentWillMount', () => {
+ function BrokenComponentWillMountWithContext() {
+ return {
+ getChildContext() {
+ return {foo: 42};
+ },
+ render() {
+ return {this.props.children}
;
+ },
+ UNSAFE_componentWillMount() {
+ throw new Error('Hello');
+ },
+ };
+ }
+ BrokenComponentWillMountWithContext.childContextTypes = {
+ foo: PropTypes.number,
+ };
+
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ });
+
+ it('mounts the error message if mounting fails', () => {
+ function renderError(error) {
+ return ;
+ }
+
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ 'ErrorBoundary componentDidMount',
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorMessage constructor',
+ 'ErrorMessage componentWillMount',
+ 'ErrorMessage render',
+ 'ErrorMessage componentDidMount',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ 'ErrorMessage componentWillUnmount',
+ ]);
+ });
+
+ it('propagates errors on retry on mounting', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'RetryErrorBoundary constructor',
+ 'RetryErrorBoundary componentWillMount',
+ 'RetryErrorBoundary render',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // In Fiber, failed error boundaries render null before attempting to recover
+ 'RetryErrorBoundary componentDidMount',
+ 'RetryErrorBoundary componentDidCatch [!]',
+ 'ErrorBoundary componentDidMount',
+ // Retry
+ 'RetryErrorBoundary render',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // This time, the error propagates to the higher boundary
+ 'RetryErrorBoundary componentWillUnmount',
+ 'ErrorBoundary componentDidCatch',
+ // Render the error
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('propagates errors inside boundary during componentWillMount', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenComponentWillMountErrorBoundary constructor',
+ 'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
+ // The error propagates to the higher boundary
+ 'ErrorBoundary componentDidMount',
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('propagates errors inside boundary while rendering error state', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenRenderErrorBoundary constructor',
+ 'BrokenRenderErrorBoundary componentWillMount',
+ 'BrokenRenderErrorBoundary render success',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // The first error boundary catches the error
+ // It adjusts state but throws displaying the message
+ // Finish mounting with null children
+ 'BrokenRenderErrorBoundary componentDidMount',
+ // Attempt to handle the error
+ 'BrokenRenderErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentDidMount',
+ 'BrokenRenderErrorBoundary render error [!]',
+ // Boundary fails with new error, propagate to next boundary
+ 'BrokenRenderErrorBoundary componentWillUnmount',
+ // Attempt to handle the error again
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('does not call componentWillUnmount when aborting initial mount', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ // Render first child
+ 'Normal constructor',
+ 'Normal componentWillMount',
+ 'Normal render',
+ // Render second child (it throws)
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // Render third child, even though an earlier sibling threw.
+ 'Normal constructor',
+ 'Normal componentWillMount',
+ 'Normal render',
+ // Finish mounting with null children
+ 'ErrorBoundary componentDidMount',
+ // Handle the error
+ 'ErrorBoundary componentDidCatch',
+ // Render the error message
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('resets callback refs if mounting aborts', () => {
+ function childRef(x) {
+ log.push('Child ref is set to ' + x);
+ }
+ function errorMessageRef(x) {
+ log.push('Error message ref is set to ' + x);
+ }
+
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // Handle error:
+ // Finish mounting with null children
+ 'ErrorBoundary componentDidMount',
+ // Handle the error
+ 'ErrorBoundary componentDidCatch',
+ // Render the error message
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'Error message ref is set to [object HTMLDivElement]',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ 'Error message ref is set to null',
+ ]);
+ });
+
+ it('resets object refs if mounting aborts', () => {
+ let childRef = React.createRef();
+ let errorMessageRef = React.createRef();
+
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // Handle error:
+ // Finish mounting with null children
+ 'ErrorBoundary componentDidMount',
+ // Handle the error
+ 'ErrorBoundary componentDidCatch',
+ // Render the error message
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+ expect(errorMessageRef.current.toString()).toEqual(
+ '[object HTMLDivElement]',
+ );
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ expect(errorMessageRef.current).toEqual(null);
+ });
+
+ it('successfully mounts if no error occurs', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+ Mounted successfully.
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Mounted successfully.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'ErrorBoundary componentDidMount',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('catches if child throws in constructor during update', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ 'Normal componentWillReceiveProps',
+ 'Normal componentWillUpdate',
+ 'Normal render',
+ // Normal2 will attempt to mount:
+ 'Normal2 constructor',
+ 'Normal2 componentWillMount',
+ 'Normal2 render',
+ // BrokenConstructor will abort rendering:
+ 'BrokenConstructor constructor [!]',
+ // Finish updating with null children
+ 'Normal componentWillUnmount',
+ 'ErrorBoundary componentDidUpdate',
+ // Handle the error
+ 'ErrorBoundary componentDidCatch',
+ // Render the error message
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('catches if child throws in componentWillMount during update', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ 'Normal componentWillReceiveProps',
+ 'Normal componentWillUpdate',
+ 'Normal render',
+ // Normal2 will attempt to mount:
+ 'Normal2 constructor',
+ 'Normal2 componentWillMount',
+ 'Normal2 render',
+ // BrokenComponentWillMount will abort rendering:
+ 'BrokenComponentWillMount constructor',
+ 'BrokenComponentWillMount componentWillMount [!]',
+ // Finish updating with null children
+ 'Normal componentWillUnmount',
+ 'ErrorBoundary componentDidUpdate',
+ // Handle the error
+ 'ErrorBoundary componentDidCatch',
+ // Render the error message
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('catches if child throws in componentWillReceiveProps during update', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+ ,
+ container,
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ 'Normal componentWillReceiveProps',
+ 'Normal componentWillUpdate',
+ 'Normal render',
+ // BrokenComponentWillReceiveProps will abort rendering:
+ 'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
+ // Finish updating with null children
+ 'Normal componentWillUnmount',
+ 'BrokenComponentWillReceiveProps componentWillUnmount',
+ 'ErrorBoundary componentDidUpdate',
+ // Handle the error
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('catches if child throws in componentWillUpdate during update', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+ ,
+ container,
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ 'Normal componentWillReceiveProps',
+ 'Normal componentWillUpdate',
+ 'Normal render',
+ // BrokenComponentWillUpdate will abort rendering:
+ 'BrokenComponentWillUpdate componentWillReceiveProps',
+ 'BrokenComponentWillUpdate componentWillUpdate [!]',
+ // Finish updating with null children
+ 'Normal componentWillUnmount',
+ 'BrokenComponentWillUpdate componentWillUnmount',
+ 'ErrorBoundary componentDidUpdate',
+ // Handle the error
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('catches if child throws in render during update', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ 'Normal componentWillReceiveProps',
+ 'Normal componentWillUpdate',
+ 'Normal render',
+ // Normal2 will attempt to mount:
+ 'Normal2 constructor',
+ 'Normal2 componentWillMount',
+ 'Normal2 render',
+ // BrokenRender will abort rendering:
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // Finish updating with null children
+ 'Normal componentWillUnmount',
+ 'ErrorBoundary componentDidUpdate',
+ // Handle the error
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('keeps refs up-to-date during updates', () => {
+ function child1Ref(x) {
+ log.push('Child1 ref is set to ' + x);
+ }
+ function child2Ref(x) {
+ log.push('Child2 ref is set to ' + x);
+ }
+ function errorMessageRef(x) {
+ log.push('Error message ref is set to ' + x);
+ }
+
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'Child1 ref is set to [object HTMLDivElement]',
+ 'ErrorBoundary componentDidMount',
+ ]);
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ // BrokenRender will abort rendering:
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // Finish updating with null children
+ 'Child1 ref is set to null',
+ 'ErrorBoundary componentDidUpdate',
+ // Handle the error
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'Error message ref is set to [object HTMLDivElement]',
+ // Child2 ref is never set because its mounting aborted
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ 'Error message ref is set to null',
+ ]);
+ });
+
+ it('recovers from componentWillUnmount errors on update', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ // Update existing child:
+ 'BrokenComponentWillUnmount componentWillReceiveProps',
+ 'BrokenComponentWillUnmount componentWillUpdate',
+ 'BrokenComponentWillUnmount render',
+ // Unmounting throws:
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
+ // Fiber proceeds with lifecycles despite errors
+ 'Normal componentWillUnmount',
+ // The components have updated in this phase
+ 'BrokenComponentWillUnmount componentDidUpdate',
+ 'ErrorBoundary componentDidUpdate',
+ // Now that commit phase is done, Fiber unmounts the boundary's children
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
+ 'ErrorBoundary componentDidCatch',
+ // The initial render was aborted, so
+ // Fiber retries from the root.
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary componentDidUpdate',
+ // The second willUnmount error should be captured and logged, too.
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ // Render an error now (stack will do it later)
+ 'ErrorBoundary render error',
+ // Attempt to unmount previous child:
+ // Done
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('recovers from nested componentWillUnmount errors on update', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+
+ ,
+ container,
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ // Update existing children:
+ 'Normal componentWillReceiveProps',
+ 'Normal componentWillUpdate',
+ 'Normal render',
+ 'BrokenComponentWillUnmount componentWillReceiveProps',
+ 'BrokenComponentWillUnmount componentWillUpdate',
+ 'BrokenComponentWillUnmount render',
+ // Unmounting throws:
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
+ // Fiber proceeds with lifecycles despite errors
+ 'BrokenComponentWillUnmount componentDidUpdate',
+ 'Normal componentDidUpdate',
+ 'ErrorBoundary componentDidUpdate',
+ 'Normal componentWillUnmount',
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
+ // Now that commit phase is done, Fiber handles errors
+ 'ErrorBoundary componentDidCatch',
+ // The initial render was aborted, so
+ // Fiber retries from the root.
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary componentDidUpdate',
+ // The second willUnmount error should be captured and logged, too.
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ // Render an error now (stack will do it later)
+ 'ErrorBoundary render error',
+ // Done
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('picks the right boundary when handling unmounting errors', () => {
+ function renderInnerError(error) {
+ return Caught an inner error: {error.message}.
;
+ }
+ function renderOuterError(error) {
+ return Caught an outer error: {error.message}.
;
+ }
+
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an inner error: Hello.');
+ expect(log).toEqual([
+ // Update outer boundary
+ 'OuterErrorBoundary componentWillReceiveProps',
+ 'OuterErrorBoundary componentWillUpdate',
+ 'OuterErrorBoundary render success',
+ // Update inner boundary
+ 'InnerErrorBoundary componentWillReceiveProps',
+ 'InnerErrorBoundary componentWillUpdate',
+ 'InnerErrorBoundary render success',
+ // Try unmounting child
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
+ // Fiber proceeds with lifecycles despite errors
+ // Inner and outer boundaries have updated in this phase
+ 'InnerErrorBoundary componentDidUpdate',
+ 'OuterErrorBoundary componentDidUpdate',
+ // Now that commit phase is done, Fiber handles errors
+ // Only inner boundary receives the error:
+ 'InnerErrorBoundary componentDidCatch',
+ 'InnerErrorBoundary componentWillUpdate',
+ // Render an error now
+ 'InnerErrorBoundary render error',
+ // In Fiber, this was a local update to the
+ // inner boundary so only its hook fires
+ 'InnerErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'OuterErrorBoundary componentWillUnmount',
+ 'InnerErrorBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('can recover from error state', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ // Error boundary doesn't retry by itself:
+ expect(container.textContent).toBe('Caught an error: Hello.');
+
+ // Force the success path:
+ log.length = 0;
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).not.toContain('Caught an error');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ // Mount children:
+ 'Normal constructor',
+ 'Normal componentWillMount',
+ 'Normal render',
+ // Finalize updates:
+ 'Normal componentDidMount',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ 'Normal componentWillUnmount',
+ ]);
+ });
+
+ it('can update multiple times in error state', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+
+ ReactDOM.render(Other screen
, container);
+ expect(container.textContent).toBe('Other screen');
+
+ ReactDOM.unmountComponentAtNode(container);
+ });
+
+ it("doesn't get into inconsistent state during removals", () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+
+ ReactDOM.render(, container);
+ expect(container.textContent).toBe('Caught an error: Hello.');
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it("doesn't get into inconsistent state during additions", () => {
+ const container = document.createElement('div');
+ ReactDOM.render(, container);
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it("doesn't get into inconsistent state during reorders", () => {
+ function getAMixOfNormalAndBrokenRenderElements() {
+ const elements = [];
+ for (let i = 0; i < 100; i++) {
+ elements.push();
+ }
+ elements.push();
+
+ let currentIndex = elements.length;
+ while (0 !== currentIndex) {
+ const randomIndex = Math.floor(Math.random() * currentIndex);
+ currentIndex -= 1;
+ const temporaryValue = elements[currentIndex];
+ elements[currentIndex] = elements[randomIndex];
+ elements[randomIndex] = temporaryValue;
+ }
+ return elements;
+ }
+
+ class MaybeBrokenRender extends React.Component {
+ render() {
+ if (fail) {
+ throw new Error('Hello');
+ }
+ return {this.props.children}
;
+ }
+ }
+
+ let fail = false;
+ const container = document.createElement('div');
+ ReactDOM.render(
+ {getAMixOfNormalAndBrokenRenderElements()},
+ container,
+ );
+ expect(container.textContent).not.toContain('Caught an error');
+
+ fail = true;
+ ReactDOM.render(
+ {getAMixOfNormalAndBrokenRenderElements()},
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('catches errors originating downstream', () => {
+ let fail = false;
+ class Stateful extends React.Component {
+ state = {shouldThrow: false};
+
+ render() {
+ if (fail) {
+ log.push('Stateful render [!]');
+ throw new Error('Hello');
+ }
+ return {this.props.children}
;
+ }
+ }
+
+ let statefulInst;
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+ (statefulInst = inst)} />
+ ,
+ container,
+ );
+
+ log.length = 0;
+ expect(() => {
+ fail = true;
+ statefulInst.forceUpdate();
+ }).not.toThrow();
+
+ expect(log).toEqual([
+ 'Stateful render [!]',
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('catches errors in componentDidMount', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+
+
+ ,
+ container,
+ );
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenComponentWillUnmount constructor',
+ 'BrokenComponentWillUnmount componentWillMount',
+ 'BrokenComponentWillUnmount render',
+ 'Normal constructor',
+ 'Normal componentWillMount',
+ 'Normal render',
+ 'BrokenComponentDidMount constructor',
+ 'BrokenComponentDidMount componentWillMount',
+ 'BrokenComponentDidMount render',
+ 'LastChild constructor',
+ 'LastChild componentWillMount',
+ 'LastChild render',
+ // Start flushing didMount queue
+ 'Normal componentDidMount',
+ 'BrokenComponentWillUnmount componentDidMount',
+ 'BrokenComponentDidMount componentDidMount [!]',
+ // Continue despite the error
+ 'LastChild componentDidMount',
+ 'ErrorBoundary componentDidMount',
+ // Now we are ready to handle the error
+ // Safely unmount every child
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
+ // Continue unmounting safely despite any errors
+ 'Normal componentWillUnmount',
+ 'BrokenComponentDidMount componentWillUnmount',
+ 'LastChild componentWillUnmount',
+ // Handle the error
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ // The willUnmount error should be captured and logged, too.
+ 'ErrorBoundary componentDidUpdate',
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ // The update has finished
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('catches errors in componentDidUpdate', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ 'BrokenComponentDidUpdate componentWillReceiveProps',
+ 'BrokenComponentDidUpdate componentWillUpdate',
+ 'BrokenComponentDidUpdate render',
+ // All lifecycles run
+ 'BrokenComponentDidUpdate componentDidUpdate [!]',
+ 'ErrorBoundary componentDidUpdate',
+ 'BrokenComponentDidUpdate componentWillUnmount',
+ // Then, error is handled
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('propagates errors inside boundary during componentDidMount', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+ (
+ We should never catch our own error: {error.message}.
+ )}
+ />
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenComponentDidMountErrorBoundary constructor',
+ 'BrokenComponentDidMountErrorBoundary componentWillMount',
+ 'BrokenComponentDidMountErrorBoundary render success',
+ 'BrokenComponentDidMountErrorBoundary componentDidMount [!]',
+ // Fiber proceeds with the hooks
+ 'ErrorBoundary componentDidMount',
+ 'BrokenComponentDidMountErrorBoundary componentWillUnmount',
+ // The error propagates to the higher boundary
+ 'ErrorBoundary componentDidCatch',
+ // Fiber retries from the root
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('calls componentDidCatch for each error that is captured', () => {
+ function renderUnmountError(error) {
+ return Caught an unmounting error: {error.message}.
;
+ }
+ function renderUpdateError(error) {
+ return Caught an updating error: {error.message}.
;
+ }
+
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+
+
+
+
+
+ ,
+ container,
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+
+
+
+ ,
+ container,
+ );
+
+ expect(container.firstChild.textContent).toBe(
+ 'Caught an unmounting error: E2.' + 'Caught an updating error: E4.',
+ );
+ expect(log).toEqual([
+ // Begin update phase
+ 'OuterErrorBoundary componentWillReceiveProps',
+ 'OuterErrorBoundary componentWillUpdate',
+ 'OuterErrorBoundary render success',
+ 'InnerUnmountBoundary componentWillReceiveProps',
+ 'InnerUnmountBoundary componentWillUpdate',
+ 'InnerUnmountBoundary render success',
+ 'InnerUpdateBoundary componentWillReceiveProps',
+ 'InnerUpdateBoundary componentWillUpdate',
+ 'InnerUpdateBoundary render success',
+ // First come the updates
+ 'BrokenComponentDidUpdate componentWillReceiveProps',
+ 'BrokenComponentDidUpdate componentWillUpdate',
+ 'BrokenComponentDidUpdate render',
+ 'BrokenComponentDidUpdate componentWillReceiveProps',
+ 'BrokenComponentDidUpdate componentWillUpdate',
+ 'BrokenComponentDidUpdate render',
+ // We're in commit phase now, deleting
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
+ // Continue despite errors, handle them after commit is done
+ 'InnerUnmountBoundary componentDidUpdate',
+ // We're still in commit phase, now calling update lifecycles
+ 'BrokenComponentDidUpdate componentDidUpdate [!]',
+ // Again, continue despite errors, we'll handle them later
+ 'BrokenComponentDidUpdate componentDidUpdate [!]',
+ 'InnerUpdateBoundary componentDidUpdate',
+ 'OuterErrorBoundary componentDidUpdate',
+ // After the commit phase, attempt to recover from any errors that
+ // were captured
+ 'BrokenComponentDidUpdate componentWillUnmount',
+ 'BrokenComponentDidUpdate componentWillUnmount',
+ 'InnerUnmountBoundary componentDidCatch',
+ 'InnerUnmountBoundary componentDidCatch',
+ 'InnerUpdateBoundary componentDidCatch',
+ 'InnerUpdateBoundary componentDidCatch',
+ 'InnerUnmountBoundary componentWillUpdate',
+ 'InnerUnmountBoundary render error',
+ 'InnerUpdateBoundary componentWillUpdate',
+ 'InnerUpdateBoundary render error',
+ 'InnerUnmountBoundary componentDidUpdate',
+ 'InnerUpdateBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'OuterErrorBoundary componentWillUnmount',
+ 'InnerUnmountBoundary componentWillUnmount',
+ 'InnerUpdateBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('discards a bad root if the root component fails', () => {
+ const X = null;
+ const Y = undefined;
+ let err1;
+ let err2;
+
+ try {
+ let container = document.createElement('div');
+ expect(() => ReactDOM.render(, container)).toWarnDev(
+ 'React.createElement: type is invalid -- expected a string ' +
+ '(for built-in components) or a class/function ' +
+ '(for composite components) but got: null.',
+ );
+ } catch (err) {
+ err1 = err;
+ }
+ try {
+ let container = document.createElement('div');
+ expect(() => ReactDOM.render(, container)).toWarnDev(
+ 'React.createElement: type is invalid -- expected a string ' +
+ '(for built-in components) or a class/function ' +
+ '(for composite components) but got: undefined.',
+ );
+ } catch (err) {
+ err2 = err;
+ }
+
+ expect(err1.message).toMatch(/got: null/);
+ expect(err2.message).toMatch(/got: undefined/);
+ });
+
+ it('renders empty output if error boundary does not handle the error', () => {
+ const container = document.createElement('div');
+ expect(() => {
+ ReactDOM.render(
+
+ Sibling
+
+
+
+
,
+ container,
+ );
+ }).toWarnDev(
+ 'ErrorBoundary: Error boundaries should implement getDerivedStateFromError()',
+ {withoutStack: true},
+ );
+ expect(container.firstChild.textContent).toBe('Sibling');
+ expect(log).toEqual([
+ 'NoopErrorBoundary constructor',
+ 'NoopErrorBoundary componentWillMount',
+ 'NoopErrorBoundary render',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // In Fiber, noop error boundaries render null
+ 'NoopErrorBoundary componentDidMount',
+ 'NoopErrorBoundary componentDidCatch',
+ // Nothing happens.
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['NoopErrorBoundary componentWillUnmount']);
+ });
+
+ it('passes first error when two errors happen in commit', () => {
+ const errors = [];
+ let caughtError;
+ class Parent extends React.Component {
+ render() {
+ return ;
+ }
+ componentDidMount() {
+ errors.push('parent sad');
+ throw new Error('parent sad');
+ }
+ }
+ class Child extends React.Component {
+ render() {
+ return ;
+ }
+ componentDidMount() {
+ errors.push('child sad');
+ throw new Error('child sad');
+ }
+ }
+
+ const container = document.createElement('div');
+ try {
+ // Here, we test the behavior where there is no error boundary and we
+ // delegate to the host root.
+ ReactDOM.render(, container);
+ } catch (e) {
+ if (e.message !== 'parent sad' && e.message !== 'child sad') {
+ throw e;
+ }
+ caughtError = e;
+ }
+
+ expect(errors).toEqual(['child sad', 'parent sad']);
+ // Error should be the first thrown
+ expect(caughtError.message).toBe('child sad');
+ });
+
+ it('propagates uncaught error inside unbatched initial mount', () => {
+ function Foo() {
+ throw new Error('foo error');
+ }
+ const container = document.createElement('div');
+ expect(() => {
+ ReactDOM.unstable_batchedUpdates(() => {
+ ReactDOM.render(, container);
+ });
+ }).toThrow('foo error');
+ });
+
+ it('handles errors that occur in before-mutation commit hook', () => {
+ const errors = [];
+ let caughtError;
+ class Parent extends React.Component {
+ getSnapshotBeforeUpdate() {
+ errors.push('parent sad');
+ throw new Error('parent sad');
+ }
+ componentDidUpdate() {}
+ render() {
+ return ;
+ }
+ }
+ class Child extends React.Component {
+ getSnapshotBeforeUpdate() {
+ errors.push('child sad');
+ throw new Error('child sad');
+ }
+ componentDidUpdate() {}
+ render() {
+ return ;
+ }
+ }
+
+ const container = document.createElement('div');
+ ReactDOM.render(, container);
+ try {
+ ReactDOM.render(, container);
+ } catch (e) {
+ if (e.message !== 'parent sad' && e.message !== 'child sad') {
+ throw e;
+ }
+ caughtError = e;
+ }
+
+ expect(errors).toEqual(['child sad', 'parent sad']);
+ // Error should be the first thrown
+ expect(caughtError.message).toBe('child sad');
+ });
+});
diff --git a/packages/react-dom/src/__tests__/ReactUpdates-test.js b/packages/react-dom/src/__tests__/ReactUpdates-test.js
index 5f3dd684e3dd3..a6b788902b1b7 100644
--- a/packages/react-dom/src/__tests__/ReactUpdates-test.js
+++ b/packages/react-dom/src/__tests__/ReactUpdates-test.js
@@ -1343,6 +1343,9 @@ describe('ReactUpdates', () => {
class ErrorBoundary extends React.Component {
componentDidCatch() {
+ // Schedule a no-op state update to avoid triggering a DEV warning in the test.
+ this.setState({});
+
this.props.parent.remount();
}
render() {
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js
index d66132574649c..0a6ae9df7bca5 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.js
@@ -46,7 +46,6 @@ import {
import {captureWillSyncRenderPlaceholder} from './ReactFiberScheduler';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
- enableGetDerivedStateFromCatch,
enableSuspense,
debugRenderPhaseSideEffects,
debugRenderPhaseSideEffectsForStrictMode,
@@ -156,6 +155,38 @@ export function reconcileChildren(
}
}
+function forceUnmountCurrentAndReconcile(
+ current: Fiber,
+ workInProgress: Fiber,
+ nextChildren: any,
+ renderExpirationTime: ExpirationTime,
+) {
+ // This function is fork of reconcileChildren. It's used in cases where we
+ // want to reconcile without matching against the existing set. This has the
+ // effect of all current children being unmounted; even if the type and key
+ // are the same, the old child is unmounted and a new child is created.
+ //
+ // To do this, we're going to go through the reconcile algorithm twice. In
+ // the first pass, we schedule a deletion for all the current children by
+ // passing null.
+ workInProgress.child = reconcileChildFibers(
+ workInProgress,
+ current.child,
+ null,
+ renderExpirationTime,
+ );
+ // In the second pass, we mount the new children. The trick here is that we
+ // pass null in place of where we usually pass the current child set. This has
+ // the effect of remounting all children regardless of whether their their
+ // identity matches.
+ workInProgress.child = reconcileChildFibers(
+ workInProgress,
+ null,
+ nextChildren,
+ renderExpirationTime,
+ );
+}
+
function updateForwardRef(
current: Fiber | null,
workInProgress: Fiber,
@@ -444,8 +475,7 @@ function finishClassComponent(
let nextChildren;
if (
didCaptureError &&
- (!enableGetDerivedStateFromCatch ||
- typeof Component.getDerivedStateFromCatch !== 'function')
+ typeof Component.getDerivedStateFromError !== 'function'
) {
// If we captured an error, but getDerivedStateFrom catch is not defined,
// unmount all the children. componentDidCatch will schedule an update to
@@ -477,20 +507,25 @@ function finishClassComponent(
// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
if (current !== null && didCaptureError) {
- // If we're recovering from an error, reconcile twice: first to delete
- // all the existing children.
- reconcileChildren(current, workInProgress, null, renderExpirationTime);
- workInProgress.child = null;
- // Now we can continue reconciling like normal. This has the effect of
- // remounting all children regardless of whether their their
- // identity matches.
+ // If we're recovering from an error, reconcile without reusing any of
+ // the existing children. Conceptually, the normal children and the children
+ // that are shown on error are two different sets, so we shouldn't reuse
+ // normal children even if their identities match.
+ forceUnmountCurrentAndReconcile(
+ current,
+ workInProgress,
+ nextChildren,
+ renderExpirationTime,
+ );
+ } else {
+ reconcileChildren(
+ current,
+ workInProgress,
+ nextChildren,
+ renderExpirationTime,
+ );
}
- reconcileChildren(
- current,
- workInProgress,
- nextChildren,
- renderExpirationTime,
- );
+
// Memoize props and state using the values we just used to render.
// TODO: Restructure so we never read values from the instance.
memoizeState(workInProgress, instance.state);
@@ -930,13 +965,6 @@ function updatePlaceholderComponent(
// suspended during the last commit. Switch to the placholder.
workInProgress.updateQueue = null;
nextDidTimeout = true;
- // If we're recovering from an error, reconcile twice: first to delete
- // all the existing children.
- reconcileChildren(current, workInProgress, null, renderExpirationTime);
- current.child = null;
- // Now we can continue reconciling like normal. This has the effect of
- // remounting all children regardless of whether their their
- // identity matches.
} else {
nextDidTimeout = !alreadyCaptured;
}
@@ -963,14 +991,28 @@ function updatePlaceholderComponent(
nextChildren = nextDidTimeout ? nextProps.fallback : children;
}
+ if (current !== null && nextDidTimeout !== workInProgress.memoizedState) {
+ // We're about to switch from the placeholder children to the normal
+ // children, or vice versa. These are two different conceptual sets that
+ // happen to be stored in the same set. Call this special function to
+ // force the new set not to match with the current set.
+ // TODO: The proper way to model this is by storing each set separately.
+ forceUnmountCurrentAndReconcile(
+ current,
+ workInProgress,
+ nextChildren,
+ renderExpirationTime,
+ );
+ } else {
+ reconcileChildren(
+ current,
+ workInProgress,
+ nextChildren,
+ renderExpirationTime,
+ );
+ }
workInProgress.memoizedProps = nextProps;
workInProgress.memoizedState = nextDidTimeout;
- reconcileChildren(
- current,
- workInProgress,
- nextChildren,
- renderExpirationTime,
- );
return workInProgress.child;
} else {
return null;
diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js
index ee0da92c911fc..fbacce2d2d9a8 100644
--- a/packages/react-reconciler/src/ReactFiberClassComponent.js
+++ b/packages/react-reconciler/src/ReactFiberClassComponent.js
@@ -466,10 +466,10 @@ function checkClassInstance(workInProgress: Fiber, ctor: any, newProps: any) {
name,
);
const noInstanceGetDerivedStateFromCatch =
- typeof instance.getDerivedStateFromCatch !== 'function';
+ typeof instance.getDerivedStateFromError !== 'function';
warningWithoutStack(
noInstanceGetDerivedStateFromCatch,
- '%s: getDerivedStateFromCatch() is defined as an instance method ' +
+ '%s: getDerivedStateFromError() is defined as an instance method ' +
'and will be ignored. Instead, declare it as a static method.',
name,
);
diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js
index dcb0b8c105f78..5ca08cebc0925 100644
--- a/packages/react-reconciler/src/ReactFiberScheduler.js
+++ b/packages/react-reconciler/src/ReactFiberScheduler.js
@@ -1464,7 +1464,7 @@ function dispatch(
const ctor = fiber.type;
const instance = fiber.stateNode;
if (
- typeof ctor.getDerivedStateFromCatch === 'function' ||
+ typeof ctor.getDerivedStateFromError === 'function' ||
(typeof instance.componentDidCatch === 'function' &&
!isAlreadyFailedLegacyErrorBoundary(instance))
) {
diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js
index bfd7714966e28..bba16ed0b6416 100644
--- a/packages/react-reconciler/src/ReactFiberUnwindWork.js
+++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js
@@ -14,6 +14,8 @@ import type {CapturedValue} from './ReactCapturedValue';
import type {Update} from './ReactUpdateQueue';
import type {Thenable} from './ReactFiberScheduler';
+import getComponentName from 'shared/getComponentName';
+import warningWithoutStack from 'shared/warningWithoutStack';
import {
IndeterminateComponent,
FunctionalComponent,
@@ -33,11 +35,7 @@ import {
Update as UpdateEffect,
LifecycleEffectMask,
} from 'shared/ReactSideEffectTags';
-import {
- enableGetDerivedStateFromCatch,
- enableSuspense,
- enableSchedulerTracing,
-} from 'shared/ReactFeatureFlags';
+import {enableSuspense, enableSchedulerTracing} from 'shared/ReactFeatureFlags';
import {StrictMode, ConcurrentMode} from './ReactTypeOfMode';
import {createCapturedValue} from './ReactCapturedValue';
@@ -104,28 +102,22 @@ function createClassErrorUpdate(
): Update {
const update = createUpdate(expirationTime);
update.tag = CaptureUpdate;
- const getDerivedStateFromCatch = fiber.type.getDerivedStateFromCatch;
- if (
- enableGetDerivedStateFromCatch &&
- typeof getDerivedStateFromCatch === 'function'
- ) {
+ const getDerivedStateFromError = fiber.type.getDerivedStateFromError;
+ if (typeof getDerivedStateFromError === 'function') {
const error = errorInfo.value;
update.payload = () => {
- return getDerivedStateFromCatch(error);
+ return getDerivedStateFromError(error);
};
}
const inst = fiber.stateNode;
if (inst !== null && typeof inst.componentDidCatch === 'function') {
update.callback = function callback() {
- if (
- !enableGetDerivedStateFromCatch ||
- getDerivedStateFromCatch !== 'function'
- ) {
+ if (typeof getDerivedStateFromError !== 'function') {
// To preserve the preexisting retry behavior of error boundaries,
// we keep track of which ones already failed during this batch.
// This gets reset before we yield back to the browser.
- // TODO: Warn in strict mode if getDerivedStateFromCatch is
+ // TODO: Warn in strict mode if getDerivedStateFromError is
// not defined.
markLegacyErrorBoundaryAsFailed(this);
}
@@ -135,6 +127,19 @@ function createClassErrorUpdate(
this.componentDidCatch(error, {
componentStack: stack !== null ? stack : '',
});
+ if (__DEV__) {
+ if (typeof getDerivedStateFromError !== 'function') {
+ // If componentDidCatch is the only error boundary method defined,
+ // then it needs to call setState to recover from errors.
+ // If no state update is scheduled then the boundary will swallow the error.
+ warningWithoutStack(
+ fiber.expirationTime === Sync,
+ '%s: Error boundaries should implement getDerivedStateFromError(). ' +
+ 'In that method, return a state update to display an error message or fallback UI.',
+ getComponentName(fiber.type) || 'Unknown',
+ );
+ }
+ }
};
}
return update;
@@ -364,8 +369,7 @@ function throwException(
const instance = workInProgress.stateNode;
if (
(workInProgress.effectTag & DidCapture) === NoEffect &&
- ((typeof ctor.getDerivedStateFromCatch === 'function' &&
- enableGetDerivedStateFromCatch) ||
+ (typeof ctor.getDerivedStateFromError === 'function' ||
(instance !== null &&
typeof instance.componentDidCatch === 'function' &&
!isAlreadyFailedLegacyErrorBoundary(instance)))
diff --git a/packages/react-reconciler/src/__tests__/ErrorBoundaryReconciliation-test.internal.js b/packages/react-reconciler/src/__tests__/ErrorBoundaryReconciliation-test.internal.js
new file mode 100644
index 0000000000000..8e7cbe067df56
--- /dev/null
+++ b/packages/react-reconciler/src/__tests__/ErrorBoundaryReconciliation-test.internal.js
@@ -0,0 +1,111 @@
+const jestDiff = require('jest-diff');
+
+describe('ErrorBoundaryReconciliation', () => {
+ let BrokenRender;
+ let DidCatchErrorBoundary;
+ let GetDerivedErrorBoundary;
+ let React;
+ let ReactFeatureFlags;
+ let ReactTestRenderer;
+ let span;
+
+ beforeEach(() => {
+ jest.resetModules();
+
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
+ ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
+ ReactTestRenderer = require('react-test-renderer');
+ React = require('react');
+
+ DidCatchErrorBoundary = class extends React.Component {
+ state = {error: null};
+ componentDidCatch(error) {
+ this.setState({error});
+ }
+ render() {
+ return this.state.error
+ ? React.createElement(this.props.fallbackTagName, {
+ prop: 'ErrorBoundary',
+ })
+ : this.props.children;
+ }
+ };
+
+ GetDerivedErrorBoundary = class extends React.Component {
+ state = {error: null};
+ static getDerivedStateFromError(error) {
+ return {error};
+ }
+ render() {
+ return this.state.error
+ ? React.createElement(this.props.fallbackTagName, {
+ prop: 'ErrorBoundary',
+ })
+ : this.props.children;
+ }
+ };
+
+ const InvalidType = undefined;
+ BrokenRender = ({fail}) =>
+ fail ? : ;
+
+ function toHaveRenderedChildren(renderer, children) {
+ let actual, expected;
+ try {
+ actual = renderer.toJSON();
+ expected = ReactTestRenderer.create(children).toJSON();
+ expect(actual).toEqual(expected);
+ } catch (error) {
+ return {
+ message: () => jestDiff(expected, actual),
+ pass: false,
+ };
+ }
+ return {pass: true};
+ }
+ expect.extend({toHaveRenderedChildren});
+ });
+
+ [true, false].forEach(isConcurrent => {
+ function sharedTest(ErrorBoundary, fallbackTagName) {
+ const renderer = ReactTestRenderer.create(
+
+
+ ,
+ {unstable_isConcurrent: isConcurrent},
+ );
+ if (isConcurrent) {
+ renderer.unstable_flushAll();
+ }
+ expect(renderer).toHaveRenderedChildren();
+
+ expect(() => {
+ renderer.update(
+
+
+ ,
+ );
+ if (isConcurrent) {
+ renderer.unstable_flushAll();
+ }
+ }).toWarnDev(isConcurrent ? ['invalid', 'invalid'] : ['invalid']);
+ expect(renderer).toHaveRenderedChildren(
+ React.createElement(fallbackTagName, {prop: 'ErrorBoundary'}),
+ );
+ }
+
+ describe(isConcurrent ? 'concurrent' : 'sync', () => {
+ it('componentDidCatch can recover by rendering an element of the same type', () =>
+ sharedTest(DidCatchErrorBoundary, 'span'));
+
+ it('componentDidCatch can recover by rendering an element of a different type', () =>
+ sharedTest(DidCatchErrorBoundary, 'div'));
+
+ it('getDerivedStateFromError can recover by rendering an element of the same type', () =>
+ sharedTest(GetDerivedErrorBoundary, 'span'));
+
+ it('getDerivedStateFromError can recover by rendering an element of a different type', () =>
+ sharedTest(GetDerivedErrorBoundary, 'div'));
+ });
+ });
+});
diff --git a/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js
index 1e98f62454552..5260851a9e324 100644
--- a/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js
@@ -2398,7 +2398,10 @@ describe('ReactIncremental', () => {
instance.setState({
throwError: true,
});
- ReactNoop.flush();
+ expect(ReactNoop.flush).toWarnDev(
+ 'Error boundaries should implement getDerivedStateFromError()',
+ {withoutStack: true},
+ );
});
it('should not recreate masked context unless inputs have changed', () => {
diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
index 982d0b641bcbe..eab804cedb39e 100644
--- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
@@ -19,7 +19,6 @@ describe('ReactIncrementalErrorHandling', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
- ReactFeatureFlags.enableGetDerivedStateFromCatch = true;
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
PropTypes = require('prop-types');
@@ -41,6 +40,99 @@ describe('ReactIncrementalErrorHandling', () => {
}
it('recovers from errors asynchronously', () => {
+ class ErrorBoundary extends React.Component {
+ state = {error: null};
+ static getDerivedStateFromError(error) {
+ ReactNoop.yield('getDerivedStateFromError');
+ return {error};
+ }
+ render() {
+ if (this.state.error) {
+ ReactNoop.yield('ErrorBoundary (catch)');
+ return ;
+ }
+ ReactNoop.yield('ErrorBoundary (try)');
+ return this.props.children;
+ }
+ }
+
+ function ErrorMessage(props) {
+ ReactNoop.yield('ErrorMessage');
+ return ;
+ }
+
+ function Indirection(props) {
+ ReactNoop.yield('Indirection');
+ return props.children || null;
+ }
+
+ function BadRender() {
+ ReactNoop.yield('throw');
+ throw new Error('oops!');
+ }
+
+ ReactNoop.render(
+
+
+
+
+
+
+
+
+
+
+ ,
+ );
+
+ // Start rendering asynchronsouly
+ ReactNoop.flushThrough([
+ 'ErrorBoundary (try)',
+ 'Indirection',
+ 'Indirection',
+ 'Indirection',
+ // An error is thrown. React keeps rendering asynchronously.
+ 'throw',
+ ]);
+
+ // Still rendering async...
+ ReactNoop.flushThrough(['Indirection']);
+
+ ReactNoop.flushThrough([
+ 'Indirection',
+
+ // Call getDerivedStateFromError and re-render the error boundary, this
+ // time rendering an error message.
+ 'getDerivedStateFromError',
+ 'ErrorBoundary (catch)',
+ 'ErrorMessage',
+ ]);
+
+ // Since the error was thrown during an async render, React won't commit
+ // the result yet.
+ expect(ReactNoop.getChildren()).toEqual([]);
+
+ // Instead, it will try rendering one more time, synchronously, in case that
+ // happens to fix the error.
+ expect(ReactNoop.flushNextYield()).toEqual([
+ 'ErrorBoundary (try)',
+ 'Indirection',
+ 'Indirection',
+ 'Indirection',
+
+ // The error was thrown again. This time, React will actually commit
+ // the result.
+ 'throw',
+ 'Indirection',
+ 'Indirection',
+ 'getDerivedStateFromError',
+ 'ErrorBoundary (catch)',
+ 'ErrorMessage',
+ ]);
+ expect(ReactNoop.getChildren()).toEqual([span('Caught an error: oops!')]);
+ });
+
+ it('recovers from errors asynchronously (legacy, no getDerivedStateFromError)', () => {
class ErrorBoundary extends React.Component {
state = {error: null};
componentDidCatch(error) {
@@ -1442,10 +1534,10 @@ describe('ReactIncrementalErrorHandling', () => {
]);
});
- it('does not provide component stack to the error boundary with getDerivedStateFromCatch', () => {
+ it('does not provide component stack to the error boundary with getDerivedStateFromError', () => {
class ErrorBoundary extends React.Component {
state = {error: null};
- static getDerivedStateFromCatch(error, errorInfo) {
+ static getDerivedStateFromError(error, errorInfo) {
expect(errorInfo).toBeUndefined();
return {error};
}
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
index 2033b4e145385..40f74132dd8b3 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
@@ -1316,11 +1316,7 @@ describe('ReactSuspense', () => {
'A',
'B',
'C',
- // 'A' matched with the placeholder. It's ok to reuse children when
- // switching back. Though in a real app you probably don't want to.
- // TODO: This is wrong. The timed out children and the placeholder
- // should be siblings in async mode. Revisit in follow-up PR.
- 'Update [A]',
+ 'Mount [A]',
'Mount [B]',
'Mount [C]',
]);
diff --git a/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee b/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee
index ec0921293d53e..813082c9a552a 100644
--- a/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee
+++ b/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee
@@ -129,15 +129,15 @@ describe 'ReactCoffeeScriptClass', ->
).toWarnDev 'Foo: getDerivedStateFromProps() is defined as an instance method and will be ignored. Instead, declare it as a static method.', {withoutStack: true}
undefined
- it 'warns if getDerivedStateFromCatch is not static', ->
+ it 'warns if getDerivedStateFromError is not static', ->
class Foo extends React.Component
render: ->
div()
- getDerivedStateFromCatch: ->
+ getDerivedStateFromError: ->
{}
expect(->
ReactDOM.render(React.createElement(Foo, foo: 'foo'), container)
- ).toWarnDev 'Foo: getDerivedStateFromCatch() is defined as an instance method and will be ignored. Instead, declare it as a static method.', {withoutStack: true}
+ ).toWarnDev 'Foo: getDerivedStateFromError() is defined as an instance method and will be ignored. Instead, declare it as a static method.', {withoutStack: true}
undefined
it 'warns if getSnapshotBeforeUpdate is static', ->
diff --git a/packages/react/src/__tests__/ReactES6Class-test.js b/packages/react/src/__tests__/ReactES6Class-test.js
index 3e4dbc040dea5..bc4d33d24417f 100644
--- a/packages/react/src/__tests__/ReactES6Class-test.js
+++ b/packages/react/src/__tests__/ReactES6Class-test.js
@@ -147,9 +147,9 @@ describe('ReactES6Class', () => {
);
});
- it('warns if getDerivedStateFromCatch is not static', () => {
+ it('warns if getDerivedStateFromError is not static', () => {
class Foo extends React.Component {
- getDerivedStateFromCatch() {
+ getDerivedStateFromError() {
return {};
}
render() {
@@ -157,7 +157,7 @@ describe('ReactES6Class', () => {
}
}
expect(() => ReactDOM.render(, container)).toWarnDev(
- 'Foo: getDerivedStateFromCatch() is defined as an instance method ' +
+ 'Foo: getDerivedStateFromError() is defined as an instance method ' +
'and will be ignored. Instead, declare it as a static method.',
{withoutStack: true},
);
diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js
index ca6120b51ce4d..f7386f0d75db6 100644
--- a/packages/react/src/__tests__/ReactProfiler-test.internal.js
+++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js
@@ -36,7 +36,6 @@ function loadModules({
ReactFeatureFlags.debugRenderPhaseSideEffects = false;
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactFeatureFlags.enableProfilerTimer = enableProfilerTimer;
- ReactFeatureFlags.enableGetDerivedStateFromCatch = true;
ReactFeatureFlags.enableSchedulerTracing = enableSchedulerTracing;
ReactFeatureFlags.enableSuspense = enableSuspense;
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = replayFailedUnitOfWorkWithInvokeGuardedCallback;
@@ -985,7 +984,7 @@ describe('Profiler', () => {
);
});
- it('should accumulate actual time after an error handled by getDerivedStateFromCatch()', () => {
+ it('should accumulate actual time after an error handled by getDerivedStateFromError()', () => {
const callback = jest.fn();
const ThrowsError = () => {
@@ -995,7 +994,7 @@ describe('Profiler', () => {
class ErrorBoundary extends React.Component {
state = {error: null};
- static getDerivedStateFromCatch(error) {
+ static getDerivedStateFromError(error) {
return {error};
}
render() {
diff --git a/packages/react/src/__tests__/ReactTypeScriptClass-test.ts b/packages/react/src/__tests__/ReactTypeScriptClass-test.ts
index c738c1742bd5f..e79ab7f496756 100644
--- a/packages/react/src/__tests__/ReactTypeScriptClass-test.ts
+++ b/packages/react/src/__tests__/ReactTypeScriptClass-test.ts
@@ -397,9 +397,9 @@ describe('ReactTypeScriptClass', function() {
);
});
- it('warns if getDerivedStateFromCatch is not static', function() {
+ it('warns if getDerivedStateFromError is not static', function() {
class Foo extends React.Component {
- getDerivedStateFromCatch() {
+ getDerivedStateFromError() {
return {};
}
render() {
@@ -409,7 +409,7 @@ describe('ReactTypeScriptClass', function() {
expect(function() {
ReactDOM.render(React.createElement(Foo, {foo: 'foo'}), container);
}).toWarnDev(
- 'Foo: getDerivedStateFromCatch() is defined as an instance method ' +
+ 'Foo: getDerivedStateFromError() is defined as an instance method ' +
'and will be ignored. Instead, declare it as a static method.',
{withoutStack: true}
);
diff --git a/packages/react/src/__tests__/createReactClassIntegration-test.js b/packages/react/src/__tests__/createReactClassIntegration-test.js
index d1e82b11f39ca..efc2bf3ba49dc 100644
--- a/packages/react/src/__tests__/createReactClassIntegration-test.js
+++ b/packages/react/src/__tests__/createReactClassIntegration-test.js
@@ -459,9 +459,9 @@ describe('create-react-class-integration', () => {
);
});
- it('warns if getDerivedStateFromCatch is not static', () => {
+ it('warns if getDerivedStateFromError is not static', () => {
const Foo = createReactClass({
- getDerivedStateFromCatch() {
+ getDerivedStateFromError() {
return {};
},
render() {
@@ -471,7 +471,7 @@ describe('create-react-class-integration', () => {
expect(() =>
ReactDOM.render(, document.createElement('div')),
).toWarnDev(
- 'Component: getDerivedStateFromCatch() is defined as an instance method ' +
+ 'Component: getDerivedStateFromError() is defined as an instance method ' +
'and will be ignored. Instead, declare it as a static method.',
{withoutStack: true},
);
diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js
index aae29ada1838b..abd23679539d3 100644
--- a/packages/shared/ReactFeatureFlags.js
+++ b/packages/shared/ReactFeatureFlags.js
@@ -10,9 +10,6 @@
// Exports ReactDOM.createRoot
export const enableUserTimingAPI = __DEV__;
-// Experimental error-boundary API that can recover from errors within a single
-// render phase
-export const enableGetDerivedStateFromCatch = false;
// Suspense
export const enableSuspense = false;
// Helps identify side effects in begin-phase lifecycle hooks and setState reducers:
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js
index 69bb61eca4d47..566d271222701 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js
@@ -15,7 +15,6 @@ import typeof * as FabricFeatureFlagsType from './ReactFeatureFlags.native-fabri
export const debugRenderPhaseSideEffects = false;
export const debugRenderPhaseSideEffectsForStrictMode = false;
export const enableUserTimingAPI = __DEV__;
-export const enableGetDerivedStateFromCatch = false;
export const enableSuspense = false;
export const warnAboutDeprecatedLifecycles = false;
export const warnAboutLegacyContextAPI = __DEV__;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js b/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js
index 66f1b4714ad4d..f9b6576d4f464 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js
@@ -15,7 +15,6 @@ import typeof * as FabricFeatureFlagsType from './ReactFeatureFlags.native-fabri
export const debugRenderPhaseSideEffects = false;
export const debugRenderPhaseSideEffectsForStrictMode = false;
export const enableUserTimingAPI = __DEV__;
-export const enableGetDerivedStateFromCatch = false;
export const enableSuspense = false;
export const warnAboutDeprecatedLifecycles = false;
export const warnAboutLegacyContextAPI = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js
index be63c527a8ee4..4b12e739183d0 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js
@@ -14,7 +14,6 @@ import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.native-fb';
// Re-export dynamic flags from the fbsource version.
export const {
- enableGetDerivedStateFromCatch,
enableSuspense,
debugRenderPhaseSideEffects,
debugRenderPhaseSideEffectsForStrictMode,
diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js
index 7a2e325723a70..a92e3c0f953f5 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-oss.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js
@@ -14,7 +14,6 @@ import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.native-oss';
export const debugRenderPhaseSideEffects = false;
export const debugRenderPhaseSideEffectsForStrictMode = false;
-export const enableGetDerivedStateFromCatch = false;
export const enableSuspense = false;
export const enableUserTimingAPI = __DEV__;
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js
index 4b1b1c4c34f2a..1fb299db333fb 100644
--- a/packages/shared/forks/ReactFeatureFlags.persistent.js
+++ b/packages/shared/forks/ReactFeatureFlags.persistent.js
@@ -15,7 +15,6 @@ import typeof * as PersistentFeatureFlagsType from './ReactFeatureFlags.persiste
export const debugRenderPhaseSideEffects = false;
export const debugRenderPhaseSideEffectsForStrictMode = false;
export const enableUserTimingAPI = __DEV__;
-export const enableGetDerivedStateFromCatch = false;
export const enableSuspense = false;
export const warnAboutDeprecatedLifecycles = false;
export const warnAboutLegacyContextAPI = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
index fb828d661dfbe..07fde5086acc3 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
@@ -15,7 +15,6 @@ import typeof * as PersistentFeatureFlagsType from './ReactFeatureFlags.persiste
export const debugRenderPhaseSideEffects = false;
export const debugRenderPhaseSideEffectsForStrictMode = false;
export const enableUserTimingAPI = __DEV__;
-export const enableGetDerivedStateFromCatch = false;
export const enableSuspense = false;
export const warnAboutDeprecatedLifecycles = false;
export const warnAboutLegacyContextAPI = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
index 213d9ef75c4e9..20e510afa1b9a 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
@@ -15,7 +15,6 @@ import typeof * as PersistentFeatureFlagsType from './ReactFeatureFlags.persiste
export const debugRenderPhaseSideEffects = false;
export const debugRenderPhaseSideEffectsForStrictMode = false;
export const enableUserTimingAPI = __DEV__;
-export const enableGetDerivedStateFromCatch = false;
export const enableSuspense = true;
export const warnAboutDeprecatedLifecycles = false;
export const warnAboutLegacyContextAPI = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js
index 3f9d960d69f1a..414bc4052b2ae 100644
--- a/packages/shared/forks/ReactFeatureFlags.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.www.js
@@ -15,7 +15,6 @@ export const {
enableSuspense,
debugRenderPhaseSideEffects,
debugRenderPhaseSideEffectsForStrictMode,
- enableGetDerivedStateFromCatch,
enableSuspenseServerRenderer,
replayFailedUnitOfWorkWithInvokeGuardedCallback,
warnAboutDeprecatedLifecycles,