Browser navigation broken by use of React Error Boundaries
Asked Answered
R

6

30

When an error is thrown in our React 16 codebase, it is caught by our top-level error boundary. The ErrorBoundary component happily renders an error page when this happens.

Where the ErrorBoundary sits

   return (
     <Provider store={configureStore()}>
       <ErrorBoundary>
         <Router history={browserHistory}>{routes}</Router>
       </ErrorBoundary>
     </Provider>
   )

However, when navigating back using the browser back button (one click), the URL changes in the address but the page does not update.

I have tried shifting the error boundary down the component tree but this issue persists.

Any clues on where this issue lies?

Radiotransparent answered 5/1, 2018 at 21:49 Comment(0)
G
60

The op has probably found a resolution by now, but for the benefit of anyone else having this issue I'll explain why I think its happening and what can be done to resolve it.

This is probably occurring due to the conditional rendering in the ErrorBoundary rendering the error message even though the history has changed.

Although not shown above, the render method in the ErrorBoundary is probably similar to this:

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

  return this.props.children;
}

Where hasError is being set in the componentDidCatch lifecycle method.

Once the state in the ErrorBoundary has been set it will always render the error message until the state changes (hasError to false in the example above). The child components (the Router component in this case) will not be rendered, even when the history changes.

To resolve this, make use of the react-router withRouter higher order component, by wrapping the export of the ErrorBoundary to give it access to the history via the props:

export default withRouter(ErrorBoundary);

In the ErrorBoundary constructor retrieve the history from the props and setup a handler to listen for changes to the current location using history.listen. When the location changes (back button clicked etc.) if the component is in an error state, it is cleared enabling the children to be rendered again.

const { history } = this.props;

history.listen((location, action) => {
  if (this.state.hasError) {
    this.setState({
      hasError: false,
    });
  }
});
Glim answered 17/3, 2018 at 20:27 Comment(4)
glad it helped you both!Glim
+1. I also had to add this to not update the erred out child component on setState and throw the error again thereby set error again in the state. shouldComponentUpdate(nextProps, nextState) { return (!this.state.error || nextState.error); }Neiman
absolutely fantasticFinally
react-router v6 removed this hoc x.xOutweigh
O
11

To add to jdavies' answer above, make sure you register the history listener in a componentDidMount or useEffect (using [] to denote it has no dependencies), and unregister it in a componentWillUnmount or useEffect return statement, otherwise you may run into issues with setState getting called in an unmounted component.

Example:

  componentDidMount() {
    this.unlisten = this.props.history.listen((location, action) => {
      if (this.state.hasError) {
        this.setState({ hasError: false });
      }
    });
  }

  componentWillUnmount() {
    this.unlisten();
  }
Objectify answered 23/4, 2019 at 18:54 Comment(0)
L
3

The analog to jdavies answer for React Router 6 is:

const { pathname } = useLocation()
const originalPathname = useRef(pathname)

useEffect(() => {
  if (pathname !== originalPathname.current) {
    resetErrorBoundary()
  }
}, [pathname, resetErrorBoundary])
Legra answered 30/10, 2021 at 5:24 Comment(1)
Worked like a charm in React 18Jerrelljerri
P
1

jdavies comment is the way to go,

but, if you are confused by this, basically you make it look like this:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    const { history } = props;
    history.listen((location, action) => {
      if (this.state["hasError"]) {
        this.setState({
          hasError: false,
        });
      }
    });
    this.state = { hasError: false };
  }

       ...

then at the end of the file you add:

export default withRouter(ErrorBoundary);

(don't forget to import { withRouter } from "react-router-dom"; at top)

also, if you were using e.g. export class ErrorBoundry ... like i was, don't forget to change the import { ErrorBoundary } from "./ErrorBoundry"; to import ErrorBoundary from "./ErrorBoundry"; wherever you use it e.g. App.tsx

Psychotherapy answered 26/7, 2021 at 15:2 Comment(0)
S
1

In my case, I'm using ErrorBoundary with FallbackComponent at the top level of the tree, to catch all errors, display the error page and report the problem using a dedicated endpoint on the backend.

In order to refresh the page after history.push(...), instead of checking/modifying the state, I'm just listening for history change and calling resetErrorBoundary function as described in the docs: https://www.npmjs.com/package/react-error-boundary#errorboundary-with-fallbackcomponent-prop

const App = () => {
  return (
    <ErrorBoundary FallbackComponent={ErrorComponent} onError={...}>
      ...
    </ErrorBoundary>
  );
};
const ErrorComponent = ({ error, resetErrorBoundary }) => {
  const history = useHistory();
  history.listen(() => {
    if (typeof resetErrorBoundary === 'function') {
      resetErrorBoundary();
    }
  });
  
  return (
    ...
  );
};

export default ErrorComponent;
Serdab answered 11/12, 2023 at 0:35 Comment(0)
J
0

tl;dr wrap components where you expect errors with error boundaries but not the entire tree

Tried first @jdavies answer using withRouter but then found a better solution for my use case: Dan from the React-Team advised against using HOCs with Error Boundaries and rather use them at stragetic places.

In that Twitter thread is a debate around the pros and cons though and Dan left it open which way you should go but I found his thoughts convincing.

So what I did was to just wrap those strategic places where I expect an error and not the entire tree. I prefer this for my use case because I can throw more expressive, specific error pages than before (something went wrong vs there was an auth error).

Juliannejuliano answered 17/2, 2019 at 4:32 Comment(1)
Can you expand on the alternative to using an HoC to solve the question that was originally asked? Thanks.Barefaced

© 2022 - 2024 — McMap. All rights reserved.