Pete's Log: React Error Boundaries With Routing

Entry #2056, (Coding, Hacking, & CS stuff, Work)
(posted when I was 43 years old.)

React Router v6 was released in November. It's apparently been re-architected to be fully hooks-based and I'm mostly pretty excited about it, especially with how nested routes will work and with the ability to access params inside deeply nested components without needing to pass props to them.

That being said, I've run into one issue updating to this new version that I haven't found covered anywhere else yet.

I like to include Error Boundaries in my React apps. And since React still requires that "[o]nly class components can be error boundaries," these remain the only class components in my apps, since I've long since switched everything else to functional components.

One quirk of error boundaries and React Router is that without special handling, an error boundary will display an error forever. To ensure that the error was cleared on navigation, I had my error boundary class listen for history changes and clear the error on navigation:

componentWillMount(): void { this.unlisten = this.props.history.listen((location, action) => { if (this.state.hasError) { this.setState(defaultState); } }); } componentWillUnmount(): void { if (this.unlisten) { this.unlisten(); } }

To get access to the history within the component props, I had to have the class props extend React Router's RouteComponentProps and then use withRouter:

class ErrorBoundaryBase extends React.Component<RouteComponentProps & ErrorBoundaryProps, State> { // class definition ... } export const RouterErrorBoundary = withRouter(ErrorBoundaryBase);

ErrorBoundaryProps is the interface for my own props and RouteComponentProps gives me the this.props.history that I use to listen for navigation.

React Router v6's move to a purely functional approach means that RouteComponentProps and withRouter are no more. But I still need my class-based error boundary to be able to pick up on navigation so I can clear the error when the user clicks on a link.

My solution doesn't feel quite right, but it works. I already had a functional component defined that handled actually rendering the component. So I added the logic to monitor navigation there:

const [previousLocation, setPreviousLocation] = React.useState(undefined as string | undefined); const { pathname } = useLocation(); React.useEffect(() => { if (previousLocation !== undefined) { onNavigate(); } setPreviousLocation(pathname); }, [pathname, onNavigate]);

I have to track previousLocation so it doesn't call onNavigate on initial load. useLocation is a React Router hook. Then in my Error Boundary render method, I pass an onNavigate method into the child component that clears the error:

render(): React.ReactNode { if (this.state.hasError) { return ( <RouterErrorBlock title="Unexpected Error" error={this.state.error} onNavigate={() => this.setState(defaultState)} /> ); } else { return this.props.children; } }

It's ugly but it seems to work. I'll continue keeping an eye out for a better solution.