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 };
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).
- children
- 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>;
....
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>;
....
And inside my page
// About.tsx
export function About() {
return <ToublesomeComponent />
}
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 };
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
});
}
}
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 };
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)