Error boundary with react-i18next and router
Tags: react, errors, programming
Published: 2023-08-04 09:50:15

Helloo everyone. I did some bad programming and got one page crash landed.

QA engineer raised red flag **_What the heck is this?_** :raised_eyebrow:

Now what?? 

chill. calm down. :no_mouth: meditate a bit. :expressionless: let's create an ErrorBoundary component together.

My react project support multiple langugaes. So i used [react-i18next](https://www.npmjs.com/package/react-i18next) package to display a generic error message following with error message.

I did a bit of background work on how to create a ErrorBoundary. As per [React docs](https://legacy.reactjs.org/docs/error-boundaries.html#introducing-error-boundaries):

> Error boundaries work like a JavaScript catch {} block, but for components. Only class components can be error boundaries. In practice, most of the time you’ll want to declare an error boundary component once and use it throughout your application.

based on this primary info let's start by creating a class component namedErrorBoundary. 

```typescript
import { TFunction } from 'i18next';
import { Component, ReactElement } from 'react';
import { withTranslation } from 'react-i18next';

class Boundary Component<Readonly<{
    children: ReactElement | ReactElement[];
    t: TFunction<'translation', undefined, 'translation'>;
  }>, Record<string, string | boolean>> {

  constructor(props: {
    children: ReactElement | ReactElement[];
    t: TFunction<'translation', undefined, 'translation'>;
  }) {
    super(props);

    this.state = {
      hasError: false,
      errorMessage: ''
    };
  }
  render() {
    return this.state.hasError ? (
      <fieldset>
        <legend>
          {this.props.t('common.errorBoundaryMessage')} 
        </legend>
        <pre>
          <code>{this.state.errorMessage}</code>
        </pre>
      </fieldset>
    ) : (
      this.props.children
    );
  }
}

const ErrorBoundary = withTranslation()(Boundary);
export { ErrorBoundary };
```

Woww that's literally a mounthful. if you ignore all the typings, we basically created a simple class component that accepts 2 props (for now). 

1. children
2. a t function for translation purpose.

Now we used a HOC `withTranslation` to wrap our class component for translation purposes. This step is not needed if you create a simple react app. Our component display the fieldset showing `Something went wrong` message in case of any error occured in children.

Let's out this to test:

```typescript
function ToublesomeComponent() {
   useEffect(() => {
     throw Error('I intentionally broke this.')
   }, [])

   return <h1/>Hello world</h1>
}

//App.tsx

....
return <ErrorBoundary>
  <ToublesomeComponent />
</ErrorBoundary>;
....
```

cool. :nerd_face: this display `Something went wrong` from our translations.

Now here comes the next part. We are using [react-router](https://www.npmjs.com/package/react-router) for navigation. This means, a sidebar with all routes and main section to display page contents.

So the App component is like this: 

for breivity I'm not writing full logic. just the basics.

```typescript
//App.tsx

....
return <main>
  <aside></aside>
  <section>
    <ErrorBoundary>
      <Outlet />
    </ErrorBoundary>
  </section>
</main>;
....
```

And inside my page
```typescript
// About.tsx

export function About() {
  return <ToublesomeComponent />
}
```

Cool. the page is not broken. 

but wait.. the navigation went haywire :exploding_head: . a developer's curse: You fix one thing, it will break other. :slightly_frowning_face:

key issue is, the `hasError` flag inside our ErrorBoundary class is true for all pages on error. That means we need to reset this flag on route changes. 

That's a piece of cake. Let's use `useLocation` and check the current route and prev route... :partying_face:

Hold on.. we are forgetting something. Our Errorboundary component is a class component. so we cannot use hooks inside a class component. :astonished:

Solution: create a functional HOC and return our class component from it. Then why waiting lets jump to it:

let's add location prop to our ErrorBoundary and create the HOC.

```typescript
// ErrorBoundary.tsx
import { Component, ComponentType, ReactElement } from 'react';
import { Location, useLocation } from 'react-router';

function withRouter(
  Component: ComponentType<{
    children: ReactElement | ReactElement[];
    location: Location;
    t: TFunction<'translation', undefined, 'translation'>;
  }>
) {
  function ComponentWithRouterProp(props: {
    children: ReactElement | ReactElement[];
    t: TFunction<'translation', undefined, 'translation'>;
  }) {
    const location = useLocation();
    return <Component {...props} location={location} />;
  }
  return ComponentWithRouterProp;
}

class Boundary extends Component<
  Readonly<{
    children: ReactElement | ReactElement[];
    location: Location;
    t: TFunction<'translation', undefined, 'translation'>;
  }>,
  Record<string, string | boolean>
> {
  constructor(props: {
    children: ReactElement | ReactElement[];
    location: Location;
    t: TFunction<'translation', undefined, 'translation'>;
  }) {
    super(props);

    this.state = {
      hasError: false,
      errorMessage: ''
    };
  }

....
}

const ErrorBoundary = withTranslation()(withRouter(Boundary));
export { ErrorBoundary };
```

Again.. :face_with_spiral_eyes: adfiwaeproadjp... explain..

we created the `withRouter` HOC which accept our class component as prop and return a unnamed functional component that forward the props to our class component along with location from `useLocation` hook.

fine. but we need to reset the flag.. yea i'm coming to that point.

In our class component, lets add this life-cycle method:

```typescript
 componentDidUpdate() {
    if (this.props.location.pathname !== this.state.prevPath) 
    {
      this.setState({ 
        hasError: false, 
        errorMessage: '', 
        prevPath: this.props.location.pathname 
      });
    }
  }
```

self explaining. in this method, we are checking if current path is equal to prev path or not. if not reset the state.

with this even one page is broken, we can still able to navigate to other pages.

This is how the entire component looks like:

```typescript
import { TFunction } from 'i18next';
import { Component, ComponentType, ReactElement } from 'react';
import { withTranslation } from 'react-i18next';
import { Location, useLocation } from 'react-router';
import { ErrorContainer } from './ErrorBoundary.styles';

function withRouter(
  Component: ComponentType<{
    children: ReactElement | ReactElement[];
    location: Location;
    t: TFunction<'translation', undefined, 'translation'>;
  }>
) {
  function ComponentWithRouterProp(props: {
    children: ReactElement | ReactElement[];
    t: TFunction<'translation', undefined, 'translation'>;
  }) {
    const location = useLocation();
    return <Component {...props} location={location} />;
  }
  return ComponentWithRouterProp;
}

class Boundary extends Component<
  Readonly<{
    children: ReactElement | ReactElement[];
    location: Location;
    t: TFunction<'translation', undefined, 'translation'>;
  }>,
  Record<string, string | boolean>
> {
  constructor(props: {
    children: ReactElement | ReactElement[];
    location: Location;
    t: TFunction<'translation', undefined, 'translation'>;
  }) {
    super(props);

    this.state = {
      hasError: false,
      errorMessage: ''
    };
  }

  componentDidUpdate() {
    if (this.props.location.pathname !== this.state.prevPath) {
      this.setState({ hasError: false, errorMessage: '', prevPath: this.props.location.pathname });
    }
  }

  componentDidCatch(error: Error) {
    this.setState({ hasError: true, errorMessage: error.message });
  }

  render() {
    return this.state.hasError ? (
      <ErrorContainer>
        <fieldset>
          <legend>{this.props.t('common.errorBoundaryMessage')}</legend>
          <pre>
            <code>{this.state.errorMessage}</code>
          </pre>
        </fieldset>
      </ErrorContainer>
    ) : (
      this.props.children
    );
  }
}
const ErrorBoundary = withTranslation()(withRouter(Boundary));
export { ErrorBoundary };
```

Again if you don't need translations, you can omit `withTranslation HOC, t prop`.

QA engineer is happy now :thumbsup: => I'm happy :love_you_gesture:

Hope this helps you as well..

See you again :wave: :wave:
Kiran