mirror of
https://github.com/facebook/react.git
synced 2026-02-26 07:55:55 +00:00
Enable getDerivedStateFromError (#13746)
* Removed the enableGetDerivedStateFromCatch feature flag (aka permanently enabled the feature) * Forked/copied ReactErrorBoundaries to ReactLegacyErrorBoundaries for testing componentDidCatch * Updated error boundaries tests to apply to getDerivedStateFromCatch * Renamed getDerivedStateFromCatch -> getDerivedStateFromError * Warn if boundary with only componentDidCatch swallows error * Fixed a subtle reconciliation bug with render phase error boundary
This commit is contained in:
@@ -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 <div>{this.props.children}</div>;
|
||||
}
|
||||
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', () => {
|
||||
</ErrorBoundary>,
|
||||
container,
|
||||
);
|
||||
|
||||
log.length = 0;
|
||||
ReactDOM.render(
|
||||
<ErrorBoundary>
|
||||
@@ -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',
|
||||
// Now that commit phase is done, Fiber handles errors
|
||||
'ErrorBoundary static getDerivedStateFromError',
|
||||
'ErrorBoundary componentWillUpdate',
|
||||
'ErrorBoundary render error',
|
||||
'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 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 <div>Caught an unmounting error: {error.message}.</div>;
|
||||
}
|
||||
@@ -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(
|
||||
<div>
|
||||
Sibling
|
||||
<NoopErrorBoundary>
|
||||
<BrokenRender />
|
||||
</NoopErrorBoundary>
|
||||
</div>,
|
||||
container,
|
||||
);
|
||||
expect(container.firstChild.textContent).toBe('Sibling');
|
||||
expect(() =>
|
||||
ReactDOM.render(
|
||||
<div>
|
||||
Sibling
|
||||
<NoopErrorBoundary>
|
||||
<BrokenRender />
|
||||
</NoopErrorBoundary>
|
||||
</div>,
|
||||
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(
|
||||
<InvalidErrorBoundary>
|
||||
<Throws />
|
||||
</InvalidErrorBoundary>,
|
||||
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(
|
||||
<ErrorBoundaryWithBothMethods>
|
||||
<Throws />
|
||||
</ErrorBoundaryWithBothMethods>,
|
||||
container,
|
||||
);
|
||||
expect(container.textContent).toBe('ErrorBoundary');
|
||||
expect(componentDidCatchError).toBe(thrownError);
|
||||
expect(getDerivedStateFromErrorError).toBe(thrownError);
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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() {
|
||||
|
||||
100
packages/react-reconciler/src/ReactFiberBeginWork.js
vendored
100
packages/react-reconciler/src/ReactFiberBeginWork.js
vendored
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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))
|
||||
) {
|
||||
|
||||
@@ -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<mixed> {
|
||||
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)))
|
||||
|
||||
@@ -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 ? <InvalidType /> : <span prop="BrokenRender" />;
|
||||
|
||||
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(
|
||||
<ErrorBoundary fallbackTagName={fallbackTagName}>
|
||||
<BrokenRender fail={false} />
|
||||
</ErrorBoundary>,
|
||||
{unstable_isConcurrent: isConcurrent},
|
||||
);
|
||||
if (isConcurrent) {
|
||||
renderer.unstable_flushAll();
|
||||
}
|
||||
expect(renderer).toHaveRenderedChildren(<span prop="BrokenRender" />);
|
||||
|
||||
expect(() => {
|
||||
renderer.update(
|
||||
<ErrorBoundary fallbackTagName={fallbackTagName}>
|
||||
<BrokenRender fail={true} />
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
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'));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 <ErrorMessage error={this.state.error} />;
|
||||
}
|
||||
ReactNoop.yield('ErrorBoundary (try)');
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
function ErrorMessage(props) {
|
||||
ReactNoop.yield('ErrorMessage');
|
||||
return <span prop={`Caught an error: ${props.error.message}`} />;
|
||||
}
|
||||
|
||||
function Indirection(props) {
|
||||
ReactNoop.yield('Indirection');
|
||||
return props.children || null;
|
||||
}
|
||||
|
||||
function BadRender() {
|
||||
ReactNoop.yield('throw');
|
||||
throw new Error('oops!');
|
||||
}
|
||||
|
||||
ReactNoop.render(
|
||||
<ErrorBoundary>
|
||||
<Indirection>
|
||||
<Indirection>
|
||||
<Indirection>
|
||||
<BadRender />
|
||||
<Indirection />
|
||||
<Indirection />
|
||||
</Indirection>
|
||||
</Indirection>
|
||||
</Indirection>
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
|
||||
// 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};
|
||||
}
|
||||
|
||||
@@ -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]',
|
||||
]);
|
||||
|
||||
@@ -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', ->
|
||||
|
||||
@@ -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(<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},
|
||||
);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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}
|
||||
);
|
||||
|
||||
@@ -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(<Foo foo="foo" />, 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},
|
||||
);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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__;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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__;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -15,7 +15,6 @@ export const {
|
||||
enableSuspense,
|
||||
debugRenderPhaseSideEffects,
|
||||
debugRenderPhaseSideEffectsForStrictMode,
|
||||
enableGetDerivedStateFromCatch,
|
||||
enableSuspenseServerRenderer,
|
||||
replayFailedUnitOfWorkWithInvokeGuardedCallback,
|
||||
warnAboutDeprecatedLifecycles,
|
||||
|
||||
Reference in New Issue
Block a user