20

I have a page which uses different components which is loaded using react router. Is it possible to integrate error boundary in each component as shown in the below code when I use the react router to route to different pages?

My target is to show errors particularly for individual components so that other components should work in case there is an error in one component.

Please see my code below:

index.js

import React from 'react';
import ReactDOM from 'react-dom';

import App from './App';
import registerServiceWorker from './registerServiceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

registerServiceWorker();

App.js

import React, { Component } from 'react';
import {BrowserRouter as Router, Route, Link } from 'react-router-dom';
//import ErrorBoundary from "./errorboundary";
import MyComponent1 from './component1';
import MyComponent2 from './component2';

class App extends Component {
render() {
return (
<Router>
<div style={{ backgroundColor: 'green' }}>
<div style={{ backgroundColor: '#f0f0ae', height: '30px' }}>
<Link to='/'>Link 1</Link> &#160;&#160;
<Link to='/comp1'>Link 2</Link> &#160;&#160;
<Link to='/comp2'>Link 3</Link> &#160;&#160;
</div>

<div style={{ backgroundColor: '#ffc993', height: '150px' }}>
<Route path='/' exact render={() => <MyComponent1 title="Component 1" />} />
<Route path='/comp1' render={() => <MyComponent1 title="Component 1 Again" />} />
<Route path='/comp2' render={() => <MyComponent2 title="Component 2" />} />
</div>
</div>
</Router>
);
}
}

export default App;

This is one of the component in which I want to use Error Boundary.

component1.js

import React, { Component } from 'react';
import ErrorBoundary from "./errorboundary";

class MyComponent1 extends Component {
state = {
boom: false,
};

throwError = () => this.setState({ boom: true });

render() {
const { title } = this.props;

if(this.state.boom) {
throw new Error(`${title} throw an error!`);
}

return (
<ErrorBoundary>
<input type='button' onClick={this.throwError} value={title} />
</ErrorBoundary>
)
}
}

export default MyComponent1;

This is another component in which I want to use the Error Boundary.

component2.js

import React, { Component } from 'react';
import ErrorBoundary from "./errorboundary";

class MyComponent2 extends Component {
state = {
boom: false,
};

throwError = () => this.setState({ boom: true });

render() {
const { title } = this.props;

if(this.state.boom) {
throw new Error(`${title} throw an error!`);
}

return (
<ErrorBoundary>
<input type='button' onClick={this.throwError} value={title} />
</ErrorBoundary>
)
}
}

export default MyComponent2;

This is my customized error message using error boundary when there is an error in each component.

errorboundary.js

import React, { Component } from 'react';

class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { error: null, errorInfo: null };

if(this.props.showError === false)
{
this.state.error = null;
this.state.errorInfo = null;
}
}

componentDidCatch = (error, info) => {
console.log("error did catch");
this.setState({error: error, errorInfo: info });        
}

render() {
if(this.state.errorInfo) {
return (
<div style={{ backgroundColor: '#ffcc99', color: 'white', width: '500px', height: '60px' }}>
An Error Occurred !
</div>
);
}
else {
return this.props.children;
}
}
}

export default ErrorBoundary;

Can anyone please help me? I am a newbie in React JS.

DXB-DEV
  • 547
  • 1
  • 7
  • 18

3 Answers3

19

You are not using the ErrorBoundary at the correct place. Wrapping an ErrorBoundary around input tag only make sure that if there is an error in the input, that is getting caught by the ErrorBoundary

return (
    <ErrorBoundary>   {/* errorBounday here only catches error in input */}
        <input type='button' onClick={this.throwError} value={title} />
    </ErrorBoundary>
)

You need to wrap your ErrorBoundary around components that throw the error like

<Route
          path="/comp1"
          render={() => (
            <ErrorBoundary>
              <MyComponent1 title="Component 1 Again" />
            </ErrorBoundary>
          )}
        />

In this way if the MyComponent1 throws an error it being caught by the error boundary which is the case for you. Your code would look like

class App extends React.Component {
  render() {
    return (
      <Router>
        <div style={{ backgroundColor: "green" }}>
          <div style={{ backgroundColor: "#f0f0ae", height: "30px" }}>
            <Link to="/">Link 1</Link> &#160;&#160;
            <Link to="/comp1">Link 2</Link> &#160;&#160;
            <Link to="/comp2">Link 3</Link> &#160;&#160;
          </div>

          <div style={{ backgroundColor: "#ffc993", height: "150px" }}>
            <Route
              path="/"
              exact
              render={() => (
                <ErrorBoundary>
                  <MyComponent1 title="Component 1" />
                </ErrorBoundary>
              )}
            />
            <Route
              path="/comp1"
              render={() => (
                <ErrorBoundary>
                  <MyComponent1 title="Component 1 Again" />
                </ErrorBoundary>
              )}
            />
            <Route
              path="/comp2"
              render={() => (
                <ErrorBoundary>
                  <MyComponent2 title="Component 2" />
                </ErrorBoundary>
              )}
            />
          </div>
        </div>
      </Router>
    );
  }
}

DEMO

Do read this question for understanding how to test ErrorBoundaries in codesandbox

Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • I have another question: Is there any possibility to access the state of each components in the error boundary? May be by assigning a prop to the error boundary or some other way? – DXB-DEV Mar 07 '18 at 08:44
  • You can't access a components state from any other component unless you pass it explicitly to child. You might add add a state in ErrorBoundary that you can set from your children – Shubham Khatri Mar 07 '18 at 08:48
  • OK. Fine. I can try that way.. Thank you – DXB-DEV Mar 07 '18 at 13:09
15

Wrap your main router outlets with ErrorBoundary but provide a unique key to ErrorBoundary to force it to teardown and build a new instance when the location changes:

export const AppRouter = () => {
  const location = useLocation();

  return (
    <main>
      <ErrorBoundary key={location.pathname}>
        <Switch>
          <Route path="/" component={Dashboard} />
          <Route path="/orders" component={Orders} />
        </Switch>
      </ErrorBoundary>
    </main>
  );
};

Be warned, this simple but lazy solution will cause unneccessary renders of everything within ErrorBoundary whenever the location changes. You could get around this possibly by calculating a less-frequently-changing key based on the pathname instead of using the whole pathname itself.

ilovett
  • 3,240
  • 33
  • 39
  • 1
    works perfectly. simple solution. hopefully it is also the correct way to use. Can anyone confirm? – rDroid Apr 30 '20 at 16:13
  • It worked for me as well. at first i got an error, but it was due to the fact that i was trying to use the hook from within the same component that renders the Router component (cf. https://github.com/ReactTraining/react-router/issues/7015) – sailor May 02 '20 at 16:17
  • When the `key` changes, the component will be re-instantiated. It might be an issue for navigating nested routes if you don't want the error to reset, going from `/some/module/one` to `/some/module/two` -- in this case you might just want to take the first two slashes -- `/some/module` and use that as the `key`. – ilovett May 04 '20 at 18:28
8

I think this answer by @Shubham Khatri is the correct one, I just want to complete it by providing a component that encapsulate the logic of wrapping a <Route> in an <ErrorBoundary>

const RouteWithErrorBoundary = (props) => {
    return (
        <ErrorBoundary key={props.location?.pathname}>
            <Route {...props} />
        </ErrorBoundary>
    );
};

Let us add some types with typescript :

const RouteWithErrorBoundary: React.FC<RouteProps> = (props) => {
    return (
        <ErrorBoundary key={props.location?.pathname}>
            <Route {...props} />
        </ErrorBoundary>
    );
};

You can then use it very easily :

<Switch>
    <RouteWithErrorBoundary path='/' exact render={() => <MyComponent1 title="Component 1" />} />
    <RouteWithErrorBoundary path='/comp1' render={() => <MyComponent1 title="Component 1 Again" />} />
    <RouteWithErrorBoundary path='/comp2' render={() => <MyComponent2 title="Component 2" />} />
</Switch>
Stephane L
  • 2,879
  • 1
  • 34
  • 44