-
Notifications
You must be signed in to change notification settings - Fork 47.3k
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
ErrorBoundaries: Initial pass at the easy case of updates #6020
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -55,7 +55,7 @@ describe('ReactErrorBoundaries', function() { | |
expect(EventPluginHub.putListener).not.toBeCalled(); | ||
}); | ||
|
||
it('will catch exceptions in componentWillUnmount', function() { | ||
it('will catch exceptions in componentWillUnmount initial render', function() { | ||
class ErrorBoundary extends React.Component { | ||
constructor() { | ||
super(); | ||
|
@@ -236,4 +236,353 @@ describe('ReactErrorBoundaries', function() { | |
'Box componentWillUnmount', | ||
]); | ||
}); | ||
|
||
it('catches errors on update', function() { | ||
var log = []; | ||
|
||
class Box extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.state = {errorMessage: null}; | ||
} | ||
render() { | ||
if (this.state.errorMessage != null) { | ||
log.push('Box renderError'); | ||
return <div>Error: {this.state.errorMessage}</div>; | ||
} | ||
log.push('Box render'); | ||
var ref = function(x) { | ||
log.push('Inquisitive ref ' + x); | ||
}; | ||
return ( | ||
<div> | ||
<Inquisitive ref={ref} /> | ||
{this.props.angry ? <Angry /> : <div />} | ||
</div> | ||
); | ||
} | ||
unstable_handleError(e) { | ||
log.push('error handled'); | ||
this.setState({errorMessage: e.message}); | ||
} | ||
componentDidMount() { | ||
log.push('Box componentDidMount'); | ||
} | ||
componentWillUnmount() { | ||
log.push('Box componentWillUnmount'); | ||
} | ||
} | ||
|
||
class Inquisitive extends React.Component { | ||
render() { | ||
log.push('Inquisitive render'); | ||
return <div>What is love?</div>; | ||
} | ||
componentDidMount() { | ||
log.push('Inquisitive componentDidMount'); | ||
} | ||
componentWillUnmount() { | ||
log.push('Inquisitive componentWillUnmount'); | ||
} | ||
} | ||
|
||
class Angry extends React.Component { | ||
render() { | ||
log.push('Angry render'); | ||
throw new Error('Please, do not render me.'); | ||
} | ||
componentDidMount() { | ||
log.push('Angry componentDidMount'); | ||
} | ||
componentWillUnmount() { | ||
log.push('Angry componentWillUnmount'); | ||
} | ||
} | ||
|
||
var container = document.createElement('div'); | ||
ReactDOM.render(<Box angry={false} />, container); | ||
ReactDOM.render(<Box angry={true} />, container); | ||
expect(container.textContent).toBe('Error: Please, do not render me.'); | ||
expect(log).toEqual([ | ||
'Box render', | ||
'Inquisitive render', | ||
'Inquisitive componentDidMount', | ||
'Inquisitive ref [object Object]', | ||
'Box componentDidMount', | ||
'Box render', | ||
'Inquisitive ref null', | ||
'Inquisitive render', | ||
'Angry render', | ||
'error handled', | ||
'Inquisitive ref null', | ||
'Inquisitive componentWillUnmount', | ||
'Box renderError', | ||
]); | ||
}); | ||
|
||
it('catches componentWillUnmount errors on update', function() { | ||
var log = []; | ||
|
||
class ErrorBoundary extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.state = {errorMessage: null}; | ||
} | ||
render() { | ||
if (this.state.errorMessage != null) { | ||
log.push('Box renderError'); | ||
return <div>Error: I am now a sad component :(</div>; | ||
} | ||
log.push('Box render'); | ||
|
||
return ( | ||
<div> | ||
<BrokenUnmount /> | ||
<BrokenUnmount /> | ||
{this.props.angry ? null : <BrokenUnmount />} | ||
</div> | ||
); | ||
} | ||
unstable_handleError(e) { | ||
log.push('error handled'); | ||
this.setState({errorMessage: e.message}); | ||
} | ||
componentDidMount() { | ||
log.push('Box componentDidMount'); | ||
} | ||
componentWillUnmount() { | ||
log.push('Box componentWillUnmount'); | ||
} | ||
} | ||
|
||
class BrokenUnmount extends React.Component { | ||
render() { | ||
return <div>text</div>; | ||
} | ||
componentWillUnmount() { | ||
log.push('BrokenUnmount is attempting to unmount'); | ||
throw new Error('Always broken.'); | ||
} | ||
} | ||
|
||
var container = document.createElement('div'); | ||
ReactDOM.render(<ErrorBoundary angry={false} />, container); | ||
ReactDOM.render(<ErrorBoundary angry={true} />, container); | ||
expect(container.textContent).toBe('Error: I am now a sad component :('); | ||
expect(log).toEqual([ | ||
'Box render', | ||
'Box componentDidMount', | ||
'Box render', | ||
'BrokenUnmount is attempting to unmount', | ||
'error handled', | ||
'BrokenUnmount is attempting to unmount', | ||
'BrokenUnmount is attempting to unmount', | ||
'BrokenUnmount is attempting to unmount', | ||
'Box renderError', | ||
]); | ||
ReactDOM.unmountComponentAtNode(container); | ||
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. Can you add 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. Fixed. |
||
expect(log).toEqual([ | ||
'Box render', | ||
'Box componentDidMount', | ||
'Box render', | ||
'BrokenUnmount is attempting to unmount', | ||
'error handled', | ||
'BrokenUnmount is attempting to unmount', | ||
'BrokenUnmount is attempting to unmount', | ||
'BrokenUnmount is attempting to unmount', | ||
'Box renderError', | ||
'Box componentWillUnmount', | ||
]); | ||
}); | ||
|
||
it('catches componentWillUnmount errors nested children', function() { | ||
class ErrorBoundary extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.state = {errorMessage: null}; | ||
} | ||
render() { | ||
if (this.state.errorMessage != null) { | ||
return <div>Error: I am now a sad component :(</div>; | ||
} | ||
|
||
return ( | ||
<div> | ||
<InnocentParent /> | ||
{this.props.angry ? null : <InnocentParent />} | ||
</div> | ||
); | ||
} | ||
unstable_handleError(e) { | ||
this.setState({errorMessage: e.message}); | ||
} | ||
} | ||
|
||
class InnocentParent extends React.Component { | ||
render() { | ||
return <BrokenUnmount />; | ||
} | ||
} | ||
|
||
class BrokenUnmount extends React.Component { | ||
render() { | ||
return <div>text</div>; | ||
} | ||
componentWillUnmount() { | ||
throw new Error('Always broken.'); | ||
} | ||
} | ||
|
||
var container = document.createElement('div'); | ||
ReactDOM.render(<ErrorBoundary angry={false} />, container); | ||
ReactDOM.render(<ErrorBoundary angry={true} />, container); | ||
expect(container.textContent).toBe('Error: I am now a sad component :('); | ||
ReactDOM.unmountComponentAtNode(container); | ||
}); | ||
|
||
it('doesn\'t get into inconsistent state during removals', function() { | ||
class ErrorBoundary extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.state = {errorMessage: null}; | ||
} | ||
render() { | ||
if (this.state.errorMessage != null) { | ||
return <div>Error: I am now a sad component :(</div>; | ||
} | ||
|
||
return ( | ||
<div>{this.props.children}</div> | ||
); | ||
} | ||
unstable_handleError(e) { | ||
this.setState({errorMessage: e.message}); | ||
} | ||
} | ||
|
||
class InnocentComponent extends React.Component { | ||
render() { | ||
return <div>text</div>; | ||
} | ||
} | ||
|
||
class GuiltyComponent extends React.Component { | ||
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. Call this BrokenUnmount here or GuiltyComponent everywhere for consistency? 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. Done. |
||
render() { | ||
return <div>text</div>; | ||
} | ||
componentWillUnmount() { | ||
throw new Error('Always broken.'); | ||
} | ||
} | ||
|
||
var container = document.createElement('div'); | ||
ReactDOM.render(<ErrorBoundary><InnocentComponent /><GuiltyComponent /><InnocentComponent /></ErrorBoundary>, container); | ||
ReactDOM.render(<ErrorBoundary />, container); | ||
expect(container.textContent).toBe('Error: I am now a sad component :('); | ||
ReactDOM.unmountComponentAtNode(container); | ||
}); | ||
|
||
it('doesn\'t get into inconsistent state during additions', function() { | ||
class ErrorBoundary extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.state = {errorMessage: null}; | ||
} | ||
render() { | ||
if (this.state.errorMessage != null) { | ||
return <div>Error: I am now a sad component :(</div>; | ||
} | ||
|
||
return ( | ||
<div>{this.props.children}</div> | ||
); | ||
} | ||
unstable_handleError(e) { | ||
this.setState({errorMessage: e.message}); | ||
} | ||
} | ||
|
||
class InnocentComponent extends React.Component { | ||
render() { | ||
return <div>text</div>; | ||
} | ||
} | ||
|
||
class GuiltyComponent extends React.Component { | ||
render() { | ||
throw new Error('Always broken.'); | ||
} | ||
} | ||
|
||
var container = document.createElement('div'); | ||
ReactDOM.render(<ErrorBoundary />, container); | ||
ReactDOM.render(<ErrorBoundary><InnocentComponent /><GuiltyComponent /><InnocentComponent /></ErrorBoundary>, container); | ||
expect(container.textContent).toBe('Error: I am now a sad component :('); | ||
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. Could we test that refs/componentDidMount are not called on any of the mounting children but that they are called for the displayed error message? 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. Done. |
||
ReactDOM.unmountComponentAtNode(container); | ||
}); | ||
|
||
it('doesn\'t get into inconsistent state during reorders', function() { | ||
|
||
function generateElements() { | ||
var elements = []; | ||
for (var i = 0; i < 100; i++) { | ||
elements.push(<InnocentComponent key={i} />); | ||
} | ||
elements.push(<GuiltyComponent key={100} />); | ||
|
||
var currentIndex = elements.length; | ||
while (0 !== currentIndex) { | ||
var randomIndex = Math.floor(Math.random() * currentIndex); | ||
currentIndex -= 1; | ||
var temporaryValue = elements[currentIndex]; | ||
elements[currentIndex] = elements[randomIndex]; | ||
elements[randomIndex] = temporaryValue; | ||
} | ||
return elements; | ||
} | ||
|
||
class ErrorBoundary extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.state = {errorMessage: null}; | ||
} | ||
render() { | ||
if (this.state.errorMessage != null) { | ||
return <div>Error: I am now a sad component :(</div>; | ||
} | ||
|
||
return ( | ||
<div>{this.props.children}</div> | ||
); | ||
} | ||
unstable_handleError(e) { | ||
this.setState({errorMessage: e.message}); | ||
} | ||
} | ||
|
||
class InnocentComponent extends React.Component { | ||
render() { | ||
return <div>text</div>; | ||
} | ||
} | ||
|
||
class GuiltyComponent extends React.Component { | ||
render() { | ||
if (fail) { | ||
throw new Error('Always broken.'); | ||
} | ||
return <div>text</div>; | ||
} | ||
} | ||
|
||
var fail = false; | ||
|
||
var container = document.createElement('div'); | ||
ReactDOM.render(<ErrorBoundary>{generateElements()}</ErrorBoundary>, container); | ||
fail = true; | ||
ReactDOM.render(<ErrorBoundary>{generateElements()}</ErrorBoundary>, container); | ||
|
||
expect(container.textContent).toBe('Error: I am now a sad component :('); | ||
ReactDOM.unmountComponentAtNode(container); | ||
}); | ||
}); |
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.
A little unfortunate that we re-call componentWillUnmount on the component that already failed to unmount.
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.
This was fixed in #6613.