React-router stops working when error boundary catches an error in route

The codepen linked to below, uses react-router and an error boundary to catch errors within each route.

If you click on "Shop", the error is caught as expected, but the other links then no longer works. What's going on here? Why does react-router-dom seemingly stop working? What is the "correct" way to do this? Is it a problem with the <ErrorBoundary> component, or the way the route components are wrapped, or? 🤔

https://codepen.io/mpontus/pen/NyKNoL


In case something happens to the codepen link:

const { BrowserRouter, Switch, Route, NavLink } = ReactRouterDOM;

class ErrorBoundary extends React.Component {
  state = { hasError: false };

  componentDidCatch(error, info) {
    this.setState({ hasError: true });
  }

  render() {
    if (this.state.hasError) {
      return <h1>An error has occured.</h1>;
    }

    return this.props.children;
  }
}

const HomeScreen = () => <h3>Home</h3>;

const ProfileScreen = () => <h3>Profile Screen</h3>;

const ShopScreen = () => {
  throw new Error("Error during render");
};

const App = () => (
  <BrowserRouter>
    <div className="container">
      <nav className="nav nav-pills">
        <NavLink exact className="nav-link" activeClassName="active" to="/">
          Home
        </NavLink>
        <NavLink className="nav-link" activeClassName="active" to="/profile">
          Profile
        </NavLink>
        <NavLink className="nav-link" activeClassName="active" to="/shop">
          Shop
        </NavLink>
      </nav>
      <Switch>
        <Route
          exact
          path="/"
          render={() => (
            <ErrorBoundary>
              <HomeScreen />
            </ErrorBoundary>
          )}
        />
        <Route
          path="/profile"
          render={() => (
            <ErrorBoundary>
              <ProfileScreen />
            </ErrorBoundary>
          )}
        />
        <Route
          path="/shop"
          render={() => (
            <ErrorBoundary>
              <ShopScreen />
            </ErrorBoundary>
          )}
        />
      </Switch>
    </div>
  </BrowserRouter>
);

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

1 answer

  • answered 2020-02-12 23:16 Matt Carlotta

    In short, since you're reusing the ErrorBoundary for each route, it's never unmounted (this is by design). As such, its hasError state persists across each route.

    You can mitigate this by updating the state when the location changes within the ErrorBoundary component:

      componentDidUpdate(prevProps) {
        if (prevProps.location.pathname !== this.props.location.pathname) {
          this.setState({ hasError: false });
        }
      }
    

    Since you're using the render prop, you'll have to manually pass the route props to the ErrorBoundary component:

    For example:

    <Route
      exact
      path="/"
      render={props => (
        <ErrorBoundary {...props}>
          <HomeScreen {...props} />
        </ErrorBoundary>
      )}
    />
    

    Working demo (since this codesandbox is in development, it'll show an error overlay, so you'll have to close the error window to continue):

    Edit Error Boundary