Skip to content

Instantly share code, notes, and snippets.

@samuel-holt
Created October 11, 2017 04:02
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save samuel-holt/388acdecbff5e4cf50f35a6e3dcbb638 to your computer and use it in GitHub Desktop.
React Error boundaries
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')
);
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}>&times;</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>
)
}
}
// 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;
}
}
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>
);
}
}
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