DEV Community

Kiran Mantha
Kiran Mantha

Posted on

Error boundary with react-i18next and router

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

QA engineer raised red flag What the heck is this? ๐Ÿคจ

Now what??

chill. calm down. ๐Ÿ˜ถ meditate a bit. ๐Ÿ˜‘ let's create an ErrorBoundary component together.

My react project support multiple langugaes. So i used 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:

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.

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 };
Enter fullscreen mode Exit fullscreen mode

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:

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

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

//App.tsx

....
return <ErrorBoundary>
  <ToublesomeComponent />
</ErrorBoundary>;
....
Enter fullscreen mode Exit fullscreen mode

cool. ๐Ÿค“ this display Something went wrong from our translations.

Now here comes the next part. We are using 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.

//App.tsx

....
return <main>
  <aside></aside>
  <section>
    <ErrorBoundary>
      <Outlet />
    </ErrorBoundary>
  </section>
</main>;
....
Enter fullscreen mode Exit fullscreen mode

And inside my page

// About.tsx

export function About() {
  return <ToublesomeComponent />
}
Enter fullscreen mode Exit fullscreen mode

Cool. the page is not broken.

but wait.. the navigation went haywire ๐Ÿคฏ . a developer's curse: You fix one thing, it will break other. ๐Ÿ™

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... ๐Ÿฅณ

Hold on.. we are forgetting something. Our Errorboundary component is a class component. so we cannot use hooks inside a class component. ๐Ÿ˜ฒ

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.

// 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 };
Enter fullscreen mode Exit fullscreen mode

Again.. ๐Ÿ˜ตโ€๐Ÿ’ซ 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:

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

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:

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 };
Enter fullscreen mode Exit fullscreen mode

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

QA engineer is happy now ๐Ÿ‘ => I'm happy ๐ŸคŸ

Hope this helps you as well..

See you again ๐Ÿ‘‹ ๐Ÿ‘‹
Kiran

Top comments (0)