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:
Brian Vaughn
2018-09-28 13:05:01 -07:00
committed by GitHub
parent a0733fe13d
commit 806eebdaee
25 changed files with 2639 additions and 242 deletions

View File

@@ -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

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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,
);

View File

@@ -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))
) {

View File

@@ -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)))

View File

@@ -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'));
});
});
});

View File

@@ -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', () => {

View File

@@ -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};
}

View File

@@ -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]',
]);

View File

@@ -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', ->

View File

@@ -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},
);

View File

@@ -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() {

View File

@@ -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}
);

View File

@@ -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},
);

View File

@@ -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:

View File

@@ -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__;

View File

@@ -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;

View File

@@ -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,

View File

@@ -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__;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -15,7 +15,6 @@ export const {
enableSuspense,
debugRenderPhaseSideEffects,
debugRenderPhaseSideEffectsForStrictMode,
enableGetDerivedStateFromCatch,
enableSuspenseServerRenderer,
replayFailedUnitOfWorkWithInvokeGuardedCallback,
warnAboutDeprecatedLifecycles,