Created
October 11, 2017 04:02
-
-
Save samuel-holt/388acdecbff5e4cf50f35a6e3dcbb638 to your computer and use it in GitHub Desktop.
React Error boundaries
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Dashboard, DashboardPanel } from './dashboard'; | |
import { getStore } from './store'; | |
import { ErrorBoundaryAppContainer } from './errorBoundary'; | |
class App extends React.Component { | |
state = { | |
currentStore: getStore(), | |
numPanels: 7 | |
}; | |
constructor(props) { | |
super(props); | |
} | |
handleStoreUpdate = () => { | |
this.setState({ | |
currentState: getStore(), | |
numPanels: getStore().panels.length | |
}); | |
} | |
render() { | |
return ( | |
<div className="page-wrap"> | |
<ErrorBoundaryAppContainer debugMode> | |
<Dashboard panels={this.state.currentStore.panels} handleStoreUpdate={this.handleStoreUpdate} /> | |
</ErrorBoundaryAppContainer> | |
</div> | |
) | |
} | |
}; | |
ReactDOM.render( | |
<App />, | |
document.getElementById('app') | |
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { getStore, enablePanelInStore } from './store'; | |
import { Chart } from './chart'; | |
import { DashboardPanelErrorBoundary } from './errorBoundary'; | |
import { Modal } from './modal'; | |
class AddPanelModal extends React.Component { | |
constructor(props) { | |
super(props); | |
} | |
state = { | |
hasErrors: false, | |
selectedPanel: null, | |
selectedSize: 1 | |
}; | |
handleFormSubmit = (event) => { | |
event.preventDefault(); | |
// Close the modal if no errors | |
if(!this.state.hasErrors) { | |
this.props.handleCloseModal(); | |
} | |
enablePanelInStore(this.state.selectedPanel, this.state.selectedSize); | |
this.props.handleStoreUpdate(); | |
} | |
handleSelectChange = (event) => { | |
const { value } = event.currentTarget; | |
this.setState({ | |
selectedPanel: value | |
}); | |
} | |
handleSizeChange = (event) => { | |
const val = parseInt(event.currentTarget.value, 10); | |
this.setState({ | |
selectedSize: val | |
}) | |
} | |
render() { | |
return ( | |
<Modal> | |
<button className="modal-close-button" onClick={this.props.handleCloseModal}>×</button> | |
<form onSubmit={this.handleFormSubmit}> | |
<div className="form-wrapper"> | |
<label> | |
<span>Select a new tile to add to your dashboard</span> | |
</label> | |
</div> | |
<div className="form-wrapper"> | |
<select onChange={this.handleSelectChange}> | |
<option value="">Select a dashboard panel</option> | |
{getStore().inactivePanels.map(panel => <option value={panel.id}>{panel.title}</option>)} | |
</select> | |
</div> | |
<div className="form-wrapper"> | |
<label> | |
Size | |
</label> | |
</div> | |
<div className="form-wrapper"> | |
<input type="number" min="1" max="3" onChange={this.handleSizeChange} /> | |
</div> | |
<button type="submit" className="primary" disabled={this.state.selectedPanel === null}>Save</button> | |
</form> | |
</Modal> | |
); | |
} | |
} | |
class DashboardPanel extends React.Component { | |
constructor(props) { | |
super(props); | |
} | |
render() { | |
const { id, title, span } = this.props; | |
let errorClassName = ''; | |
const panelClassName = `dashboard__panel dashboard__panel--span-${span} ${errorClassName}`; | |
const goalData = getStore().data[id]; | |
const { goal, current, decrease } = goalData; | |
const currentString = current ? current.toLocaleString() : 'N/A'; | |
const goalString = goal ? goal.toLocaleString() : 'N/A'; | |
return ( | |
<div className={panelClassName}> | |
<div className="panel-header"> | |
<h3>{title}</h3> | |
</div> | |
<div className="panel-content"> | |
<div className="panel-content__chart"> | |
<Chart {...goalData} {...this.props} /> | |
</div> | |
<div className="panel-content__stats"> | |
<span>Current: </span> | |
<span className="stats-current">{currentString}</span><br/> | |
<span>Goal: </span> | |
<span className="stats-goal">{goalString}</span> | |
</div> | |
</div> | |
</div> | |
); | |
} | |
} | |
DashboardPanel.defaultProps = { | |
span: 1 | |
} | |
class AddDashboardPanel extends React.Component { | |
constructor(props) { | |
super(props); | |
} | |
state = { | |
openModal: false | |
}; | |
handleAddPanelClick = () => { | |
this.setState({ openModal: true }); | |
} | |
handleCloseModal = () => { | |
this.setState({ openModal: false }); | |
} | |
render() { | |
if(getStore().inactivePanels.length === 0) { | |
throw new Error('No more panels yo'); | |
} | |
return ( | |
<div className="dashboard__panel dashboard__panel--add"> | |
<div className="panel-header"> | |
<h3>{this.props.title}</h3> | |
</div> | |
<div className="panel-content"> | |
<div className="panel-content__add-panel" onClick={this.handleAddPanelClick}> | |
<div className="panel-add-icon">+</div> | |
</div> | |
</div> | |
{this.state.openModal && <AddPanelModal {...this.props} handleCloseModal={this.handleCloseModal} />} | |
</div> | |
); | |
} | |
} | |
AddDashboardPanel.defaultProps = { | |
title: 'Add a new tile', | |
}; | |
export class Dashboard extends React.Component { | |
constructor(props) { | |
super(props); | |
} | |
render() { | |
return ( | |
<div className="dashboard"> | |
<DashboardPanelErrorBoundary> | |
<AddDashboardPanel {...this.props} /> | |
</DashboardPanelErrorBoundary> | |
{this.props.panels.map((panel) => ( <DashboardPanel {...panel}/> ))} | |
</div> | |
) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Stateless error panel | |
const ErrorPanel = ({ currentError, errorInfo, debugMode }) => { | |
let debugMessage = ''; | |
if(errorInfo && debugMode) { | |
// Verbose output for local testing: | |
debugMessage = ( | |
<div className="local-dev-debug"> | |
<h5>Error message:</h5> | |
<pre> | |
<code> | |
{currentError.message} | |
</code> | |
</pre> | |
<h5>Error stack:</h5> | |
<pre> | |
<code> | |
{currentError.stack} | |
</code> | |
</pre> | |
<h5>Component stack:</h5> | |
<pre> | |
<code> | |
{errorInfo.componentStack} | |
</code> | |
</pre> | |
</div> | |
); | |
} | |
return ( | |
<div className="error-panel"> | |
<h4>Oh noo! Something went horribly wrong 😳</h4> | |
<p>Don't panic... It'll most likely be our enthusiastic front-end developers experimenting with bleeding-edge features again... </p> | |
<p><a href="#refresh-dat-page">Refresh the page</a>, then try performing the action again. If that doesn't work send us a message on our <a href="#goto-contact-page" target="_blank">contact page</a> and we'll look into it pronto!</p> | |
{debugMessage} | |
</div> | |
); | |
} | |
ErrorPanel.defaultProps = { | |
errorInfo: null, | |
debugMode: false | |
} | |
export class ErrorBoundaryAppContainer extends React.Component { | |
constructor(props) { | |
super(props); | |
} | |
state = { | |
currentError: null, | |
errorInfo: null, | |
}; | |
componentDidCatch(error, info) { | |
this.setState({ | |
currentError: error, | |
errorInfo: info | |
}); | |
// Send error and error info to Raygun! | |
if(!!window.rg4js) { | |
rg4js('send', error); | |
rg4js('customTags', info); | |
} | |
} | |
render() { | |
if (!!this.state.currentError) { | |
return <ErrorPanel {...this.props} currentError={this.state.currentError} errorInfo={this.state.errorInfo} />; | |
} | |
return this.props.children; | |
} | |
} | |
ErrorBoundaryAppContainer.defaultProps = { | |
debugMode: false | |
}; | |
export class DashboardPanelErrorBoundary extends React.Component { | |
constructor(props) { | |
super(props); | |
} | |
state = { | |
errorMessage: '', | |
hasError: false | |
} | |
componentDidCatch(error) { | |
this.setState({ | |
hasError: true, | |
errorMessage: error.message | |
}); | |
} | |
render() { | |
if(this.state.hasError) { | |
return( | |
<div className="dashboard__panel dashboard__panel--error"> | |
<div className="panel-header"> | |
<h3>{this.state.errorMessage}</h3> | |
</div> | |
<div className="panel-content"> | |
<div className="sad-face">😞</div> | |
</div> | |
</div> | |
); | |
} | |
return this.props.children; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ModalContainer extends React.Component { | |
constructor(props) { | |
super(props); | |
} | |
render () { | |
return ReactDOM.createPortal( | |
this.props.children, | |
document.getElementById('modal-container'), | |
); | |
} | |
} | |
export class Modal extends React.Component { | |
constructor(props) { | |
super(props); | |
} | |
state = { | |
open: false | |
} | |
render() { | |
return ( | |
<ModalContainer> | |
<div className="modal-background"></div> | |
<div className="modal-content"> | |
{this.props.children} | |
</div> | |
</ModalContainer> | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export const STORE = { | |
panels: [ | |
{ | |
id: 'steps', | |
title: 'Steps', | |
}, | |
{ | |
id: 'distance', | |
title: 'Distance (metres)', | |
}, | |
{ | |
id: 'sleep', | |
title: 'Sleep (hours)', | |
span: 2, | |
}, | |
{ | |
id: 'calories', | |
title: 'Calories', | |
}, | |
{ | |
id: 'water', | |
title: 'Water (litres)', | |
}, | |
{ | |
id: 'progress', | |
title: 'Progress', | |
span: 2 | |
}, | |
], | |
inactivePanels: [ | |
{ | |
id: 'heart_rate', | |
title: 'Heart reate (bpm)', | |
decrease: true | |
}, | |
{ | |
id: 'bmi', | |
title: 'BMI (Body mass index)', | |
decrease: true | |
}, | |
{ | |
id: 'weight', | |
title: 'Weight', | |
decrease: true | |
}, | |
{ | |
id: 'days_exercised', | |
title: 'Days exercised' | |
} | |
], | |
data: { | |
steps: { | |
goal: 10000, | |
current: 2274, | |
}, | |
distance: { | |
goal: 5000, | |
current: 1509, | |
}, | |
sleep: { | |
goal: 8, | |
current: 5 | |
}, | |
calories: { | |
goal: 20000, | |
current: 19280, | |
decrease: true | |
}, | |
water: { | |
goal: 2.25, | |
current: 1.5 | |
}, | |
progress: { | |
goal: 100, | |
current: 22 | |
}, | |
heart_rate: { | |
goal: 52, | |
current: 55, | |
decrease: true, | |
}, | |
bmi: { | |
goal: 20.5, | |
current: 25.5, | |
decrease: true, | |
}, | |
weight: { | |
goal: 90, | |
current: 85 | |
}, | |
days_exercised: { | |
goal: 5, | |
current: 2 | |
} | |
} | |
}; | |
// Rudimentary store simulator | |
let CURRENT_STORE = STORE; | |
export const enablePanelInStore = (id, size=1) => { | |
const currentStore = getStore(); | |
let panel = currentStore.inactivePanels.find(p => p.id === id); | |
panel.span = size; | |
const inactivePanelIndex = CURRENT_STORE.inactivePanels.findIndex(i => i.id === id); | |
CURRENT_STORE.inactivePanels.splice(inactivePanelIndex, 1); | |
currentStore.panels.unshift(panel); | |
} | |
export const getStore = () => { | |
return CURRENT_STORE; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment