Helloo everyone. I did some bad programming and got one page crash landed.
QA engineer raised red flagWhat 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 usedreact-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 perReact 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';classBoundaryComponent<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(){returnthis.state.hasError?(<fieldset><legend>{this.props.t('common.errorBoundaryMessage')}</legend><pre><code>{this.state.errorMessage}</code></pre></fieldset>):(this.props.children);}}constErrorBoundary=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 HOCwithTranslation
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 showingSomething went wrong
message in case of any error occured in children.
Let's out this to test:
functionToublesomeComponent(){useEffect(()=>{throwError('I intentionally broke this.')},[])return<h1/>Helloworld</h1>}//App.tsx....return<ErrorBoundary><ToublesomeComponent/></ErrorBoundary>;....
cool. 🤓 this displaySomething went wrong
from our translations.
Now here comes the next part. We are usingreact-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.tsxexportfunctionAbout(){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, thehasError
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 useuseLocation
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.tsximport{Component,ComponentType,ReactElement}from'react';import{Location,useLocation}from'react-router';functionwithRouter(Component:ComponentType<{children:ReactElement|ReactElement[];location:Location;t:TFunction<'translation',undefined,'translation'>;}>){functionComponentWithRouterProp(props:{children:ReactElement|ReactElement[];t:TFunction<'translation',undefined,'translation'>;}){constlocation=useLocation();return<Component{...props}location={location}/>;}returnComponentWithRouterProp;}classBoundaryextendsComponent<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:''};}....}constErrorBoundary=withTranslation()(withRouter(Boundary));export{ErrorBoundary};
Again.. 😵💫 adfiwaeproadjp... explain..
we created thewithRouter
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 fromuseLocation
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';functionwithRouter(Component:ComponentType<{children:ReactElement|ReactElement[];location:Location;t:TFunction<'translation',undefined,'translation'>;}>){functionComponentWithRouterProp(props:{children:ReactElement|ReactElement[];t:TFunction<'translation',undefined,'translation'>;}){constlocation=useLocation();return<Component{...props}location={location}/>;}returnComponentWithRouterProp;}classBoundaryextendsComponent<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(){returnthis.state.hasError?(<ErrorContainer><fieldset><legend>{this.props.t('common.errorBoundaryMessage')}</legend><pre><code>{this.state.errorMessage}</code></pre></fieldset></ErrorContainer>):(this.props.children);}}constErrorBoundary=withTranslation()(withRouter(Boundary));export{ErrorBoundary};
Again if you don't need translations, you can omitwithTranslation HOC, t prop
.
QA engineer is happy now 👍 => I'm happy 🤟
Hope this helps you as well..
See you again 👋 👋
Kiran
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse