-
Notifications
You must be signed in to change notification settings - Fork 47.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enable getDerivedStateFromError #13746
Changes from 5 commits
4ed84b0
cd49672
56bbf1c
f6fb111
3df17ba
233459a
9943c6c
54f637f
574f68e
f1171b7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,21 @@ 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. | ||
const updateQueue = fiber.updateQueue; | ||
warningWithoutStack( | ||
updateQueue !== null && updateQueue.firstUpdate !== null, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check isn't sufficient because There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice, thanks! |
||
'%s: Error boundaries should implement getDerivedStateFromError(). ' + | ||
'In that method, return a state update to display an error message or fallback UI, ' + | ||
'or rethrow the error to let parent components handle it.', | ||
getComponentName(fiber.type) || 'Unknown', | ||
); | ||
} | ||
} | ||
}; | ||
} | ||
return update; | ||
|
@@ -364,8 +371,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))) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'}), | ||
); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test was useful for me since my initial "fix" to sync mode broke concurrent mode. I'm not sure about this amount of abstraction though. On the one hand, it makes it very easy to spot what's different between these otherwise very similar tests. On the other hand, it may be harder to read. |
||
|
||
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')); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not super confident about this fix ^
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, yeah I don't think this is right. I think I know how to fix it though. Let's pair on it tomorrow morning.