| leads to capture and front-end component exception handling is a very important and necessary. For React, ErrorBoundary is usually used to implement it. Today, let’s build a react-error-boundary wheel.

What happened

Hello, my friends, I am a front-end developer at ABCMouse. Just now, my boss said to me: sea monster, what happened? Why did the page go blank? I said: What’s going on? Sent me some screenshots. I opened the console and looked:

Oh! It turned out that yesterday, a young man at the back end said he wanted to intertune with me. I said yes. Then, I said: Little brother, try to get your data in the format I want:

interface User {    
  name: string;    
  age: number;
}
interface GetUserListResponse {    
retcode: number;    
data: User[]
}
Copy the code

He was unconvinced. He said, you’re useless. I said: this is useful, this is the standard, the traditional front and back end joint investigation data is to speak of the standard, can play a role in improving the quality of the project. A system of more than a million lines of code will not easily break as long as it has a type specification. He said try it. I said yes.

My request just went out. His data. Bam! And it came back! Soon!

{retcode: 0, data: [{name: 0, age: 11}, undefined, null]}Copy the code

Retcode: 0 = falsy = falsy = falsy = falsy = falsy = falsy = falsy = falsy = falsy = falsy = falsy = falsy = falsy = falsy = falsy = falsy = falsy = falsy = falsy = falsy = falsy = falsy = falsy = falsy

After that comes the normal processing of business logic and page presentation. I smiled and posted the code online, ready to call it a day. Then, now that the line suddenly went blank, I looked at the data returned:

Retcode: 0, data: [{name: 'retcode ', age: 11},' retcode ', 'retcode']}Copy the code

I was careless! No type judgment! Although this is an exception on the back end, the front end should not have a blank screen. This exception should be handled using the Error Boundary convenience feature provided by React. Here is how to type this set of Error Boundary.

Step 1: Copy

Directly copy the example from the official website and output the ErrorBoundary component:

class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) {// Update state so that the next render can display the degraded UI return {hasError: true}; } componentDidCatch(error, errorInfo) {// You can also report error logs to server logger.error(error, errorInfo); } render() {if (this.state.haserror) {// You can customize the UI and render return <h1>Something went wrong. } return this.props.children; }}Copy the code

Then wrap the business components:

<ErrorBoundary> // Catch error <UserList /> // try hard to report error </ErrorBoundary>Copy the code

If there is an error in the UserList, ErrorBoundary will catch it and then update the component state in getDerivedStateFromError and render will say Something went wrong, Will not render this.props. Children.

Conclusion:

  1. Wrap ErrorBoundary around business components that could go wrong;

  2. When a business component reports an error, it calls the logic in the componentDidCatch hook, sets hasError to true, and displays it directly

Step 2: Build a flexible wheel

If you really want to build a good wheel, you should not write return

Something went wrong

/ / error display after the element type of the type FallbackElement = React. ReactElement < unknown, string | React. FC | typeof ponent React.Com > | null; // Props export interface FallbackProps {error: error; } // This component ErrorBoundary props interface ErrorBoundaryProps {fallback? : FallbackElement; onError? : (error: Error, info: string) => void; } / / this component ErrorBoundary props interface ErrorBoundaryState {error: error | null; } // initialState const initialState: ErrorBoundaryState = {Error: null, } class ErrorBoundary extends React.Component<React.PropsWithChildren<ErrorBoundaryProps>, ErrorBoundaryState> { state = initialState; static getDerivedStateFromError(error: Error) { return {error}; } componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { if (this.props.onError) { this.props.onError(error, errorInfo.componentStack); } } render() { const {fallback} = this.props; const {error} = this.state; if (error ! == null) { if (React.isValidElement(fallback)) { return fallback; } throw new Error('ErrorBoundary component needs to pass fallback'); } return this.props.children; } } export default ErrorBoundaryCopy the code

The above provides two props: onError and fallback. The former is the callback for an error and can report an error message or prompt the user, and the latter passes an error message as follows:

Const App = () => {return (<ErrorBoundary fallback={<div> error </div>} onError={logger.error(' error ')}> <UserList /> </ErrorBoundary> ) }Copy the code

This already makes ErrorBoundary a little more flexible. However, some people prefer to pass the Fallback rendering function and fallback component as props to the ErrorBoundary instead of passing a ReactElement. Therefore, in order to accommodate more people, the fallback is extended:

Const App = () => {return (<ErrorBoundary fallback={<div> error </div>} onError={logger.error(' error ')}> <UserList /> </ErrorBoundary> ) }Copy the code

There are three ways to pass in an error message component: fallback (element), FallbackComponent (component), and fallbackRender (render function). Now the wheels are more flexible:

export declare function FallbackRender (props: FallbackProps): FallbackElement; Interface ErrorBoundaryProps {fallback? : FallbackElement; // a ReactElement FallbackComponent? : React.ComponentType<FallbackProps>; // Fallback component fallbackRender? : typeof FallbackRender; // Render fallback element function onError? : (error: Error, info: string) => void; } class ErrorBoundary extends React.Component<React.PropsWithChildren<ErrorBoundaryProps>, ErrorBoundaryState> { ... render() { const {fallback, FallbackComponent, fallbackRender} = this.props; const {error} = this.state; // Multiple fallback judgments if (error! == null) { const fallbackProps: If (React. IsValidElement (fallback)) {return fallback; If (typeof fallbackRender === 'function') {return (fallbackRender as typeof) FallbackRender)(fallbackProps); } // Check if there is FallbackComponent if (FallbackComponent) {return <FallbackComponent {... FallbackProps} />} throw new Error('ErrorBoundary component needs to pass fallback, fallbackRender, one of FallbackComponent '); } return this.props.children; }}Copy the code

Conclusion:

  1. Change hasError to error and Boolean to error. It is useful to get more error information.

  2. Add fallback, FallbackComponent, and fallbackRender props to provide multiple methods to pass in fallback.

Step 3: Add the reset callback

Sometimes you get a situation where the server suddenly gets a blowdown, 503, 502, and the front end doesn’t get a response, and then one of the components reports an error, and then it’s fine again. A better way to do this is to allow the user to click a button in the fallback to reload the offending component without having to reload the page, which is referred to below as a “reset.”

At the same time, some developers also need to add their own logic to the reset, such as pop-up prompts, log reporting, etc.

Illustration:

The following is an implementation of the above two requirements:

/ / error display after the element type of the type FallbackElement = React. ReactElement < unknown, string | React. FC | typeof ponent React.Com > | null; // Props export interface FallbackProps {error: error; resetErrorBoundary: () => void; ErrorBoundary props interface ErrorBoundaryProps {... onReset? : () => void; // Developer custom reset logic, Class ErrorBoundary extends React.Component<React.PropsWithChildren<ErrorBoundaryProps>, ErrorBoundaryState> { ... // Reset the component state and set error to null reset = () => {this.setState(initialState); ResetErrorBoundary = () => {if (this.props. OnReset) {this.props. OnReset (); } this.reset(); } render() { const {fallback, FallbackComponent, fallbackRender} = this.props; const {error} = this.state; if (error ! == null) { const fallbackProps: FallbackProps = { error, resetErrorBoundary: If (React. IsValidElement (fallback)) {return fallback; } if (typeof fallbackRender === 'function') { return (fallbackRender as typeof FallbackRender)(fallbackProps); } if (FallbackComponent) { return <FallbackComponent {... FallbackProps} />} throw new Error('ErrorBoundary component needs to pass fallback, fallbackRender, one of FallbackComponent '); } return this.props.children; }}Copy the code

After modification, add the reset logic code:

Const App = () => {const onError = () => logger.error(' error ') return (<div> <ErrorBoundary fallback={<div> error </div>} onError={onError}> <UserList /> </ErrorBoundary> <ErrorBoundary FallbackComponent={ErrorFallback} onError={onError}> <UserList /> </ErrorBoundary> <ErrorBoundary fallbackRender={(fallbackProps) => <ErrorFallback {... fallbackProps} />} onError={onError} > <UserList /> </ErrorBoundary> </div> ) }Copy the code

In the example above, you define the logic you want to try again in onReset, and then you bind props. ResetErrorBoudnary to reset in renderFallback, and when reset is clicked, onReset is called, Also clear the ErrorBoundary component state (set error to NULL).

Conclusion:

  1. Add onReset to implement the reset logic.

  2. Find a button in the Fallback component bound functions.reseTerrorBoundary to trigger the reset logic.

Step 4: Listen to render to reset

The reset logic above is simple and useful, but sometimes it has limitations: the action that triggers the reset can only be in the Fallback. What if my reset button isn’t in fallback? Or what if the onReset function is not even in the App component? Should onReset be transmitted all the way to this App and then into ErrorBoundary like a family heirloom?

At this point, we wonder if we can listen for state updates, reset them whenever they happen, and there’s no harm in reloading the component anyway. The state here is completely managed by global state and put into Redux.

If the objects in the props array change, the ErrorBoundary will reset, so that it is more flexible to control whether or not to reset. Do it now:

Interface ErrorBoundaryProps {... resetKeys? : Array<unknown>; } // Check if resetKeys are changed const changedArray = (a: Array<unknown> = [], b: Array<unknown> = []) => {return a.length! == b.length || a.some((item, index) => ! Object.is(item, b[index])); } class ErrorBoundary extends React.Component<React.PropsWithChildren<ErrorBoundaryProps>, ErrorBoundaryState> { ... componentDidUpdate(prevProps: Readonly<React.PropsWithChildren<ErrorBoundaryProps>>) { const {error} = this.state; const {resetKeys, onResetKeysChange} = this.props; // If (changedArray(prevProps. ResetKeys, resetKeys)) {// Reset the ErrorBoundary state, And call the onReset callback this.reset(); } } render() { ... }}Copy the code

First, in componentDidupdate to do the resetKeys listener, as long as the component has render to see if the elements in resetKeys changed, changed will reset.

But here’s another problem: what if the element in resetKeys is a Date or an object? So, we also need to give developers a way to determine if the resetKeys element has changed, so here we can add an onResetKeysChange props:

Interface ErrorBoundaryProps {... resetKeys? : Array<unknown>; onResetKeysChange? : ( prevResetKey: Array<unknown> | undefined, resetKeys: Array<unknown> | undefined, ) => void; } class ErrorBoundary extends React.Component<React.PropsWithChildren<ErrorBoundaryProps>, ErrorBoundaryState> { ... componentDidUpdate(prevProps: Readonly<React.PropsWithChildren<ErrorBoundaryProps>>) { const {resetKeys, onResetKeysChange} = this.props; if (changedArray(prevProps.resetKeys, resetKeys)) { if (onResetKeysChange) { onResetKeysChange(prevProps.resetKeys, resetKeys); } // Reset the ErrorBoundary state and call onReset to call this.reset(); } } render() { ... }}Copy the code

After the changedArray check, use props. OnResetKeysChange again to customize the check (if any) to see if the element value in resetKeys has been updated.

Any other questions? Well, another question. ComponentDidUpdate hook = componentDidUpdate hook = key ();

  1. XxxKey raises an error.

  2. [Fixed] A component error caused something to change in the resetKeys.

  3. ComponentDidUpdate found something updated in resetKeys.

  4. After the reset, the component that reported the error is displayed, because the error still exists (or has not been resolved), and the component that reported the error raises the error again.

  5. .

We need to distinguish between error and normal component render, and we need to make sure that there is no error before resetting it. The specific implementation idea is shown in the figure:

The concrete implementation is as follows:

class ErrorBoundary extends React.Component<React.PropsWithChildren<ErrorBoundaryProps>, ErrorBoundaryState> { state = initialState; // Render /update updatedWithError = false; static getDerivedStateFromError(error: Error) { return {error}; } componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { if (this.props.onError) { this.props.onError(error, errorInfo.componentStack); } } componentDidUpdate(prevProps: Readonly<React.PropsWithChildren<ErrorBoundaryProps>>) { const {error} = this.state; const {resetKeys, onResetKeysChange} = this.props; // Set flag=true and do not reset if (error! == null && ! this.updatedWithError) { this.updatedWithError = true; return; } // Check if resetKeys have been changed, and reset if (error! == null && changedArray(prevProps.resetKeys, resetKeys)) { if (onResetKeysChange) { onResetKeysChange(prevProps.resetKeys, resetKeys); } this.reset(); } } reset = () => { this.updatedWithError = false; this.setState(initialState); } resetErrorBoundary = () => { if (this.props.onReset) { this.props.onReset(); } this.reset(); } render() { ... }}Copy the code

The changes above are:

  1. Use updatedWithError as a flag to determine if render/update has been raised due to error;

  2. If there is no current error, it will not reset anyway;

  3. Render /update: set updatedWithError= true; render/update: set updatedWithError= true;

  4. Each update: there is an error, and if updatedWithError is true, it has been updated due to error. Future updates will be reset whenever something changes in the resetKeys.

At this point, we have two ways to do this:

methods

Trigger range

Usage scenarios

Thought burden

Call resetErrorBoundary manually

Typically in fallback components

Users can manually click “Reset” in Fallback to achieve the reset

The most direct, lighter ideological burden

Update resetKeys

I don’t care where it is, you know

The user can reset it outside of the offending component, resetKeys have data that the offending component depends on, and automatically reset it during rendering

Indirect trigger, thinking about which values to put in the resetKeys is a bit of a mental burden

Conclusion:

  1. Add resetKeys and onResetKeysChange props to enable developers to automatically reset their props for listening to value changes.

  2. In componentDidUpdate, as long as the component rendering or update is not caused by error, and the resetKeys have changed, then directly reset the component state to achieve automatic reset;

Another nice thing about automatic reset here is that if it’s an exception caused by network fluctuations, of course the page will show a fallback, and if you reset it using the props. ResetErrorBoundary method directly above, it will never reset as long as the user doesn’t hit the reset button. And because of exceptions caused by network fluctuations, it can be a problem for 0.001 seconds and otherwise, so if we put some values that change frequently in the resetKeys, it is easy to trigger the reset automatically. For example, an automatic reset is triggered when an error is reported and the element value of resetKeys changes because the value elsewhere has changed. For the user, the most you’ll see is a flash of a Fallback, and then it’s back to normal. This way, users don’t need to trigger the reset themselves.

Step 5: Output the wheel

In the fourth step above, the component is output by export default ErrorBoundary at the end. If there is a catch error in many places in the agent, there will be such a long-winded code:

<div>
  <ErrorBoundary>
    <AAA/>
  </ErrorBoundary>
  <ErrorBoundary>
    <BBB/>
  </ErrorBoundary>
  <ErrorBoundary>
    <CCC/>
  </ErrorBoundary>
  <ErrorBoundary>
    <DDD/>
  </ErrorBoundary>
</div>
Copy the code

To handle this verbose package, we can use the React Router withRouter function. We can also print a higher-order function withErrorBoundary:

/** * with * @param Component * @param errorBoundaryProps error props */ function withErrorBoundary<P> (Component: React.ComponentType<P>, errorBoundaryProps: ErrorBoundaryProps): React.ComponentType<P> { const Wrapped: React.ComponentType<P> = props => { return ( <ErrorBoundary {... errorBoundaryProps}> <Component {... Props} / > < / ErrorBoundary >)} / / DevTools showed the components of the const name = Component. The displayName | | Component. The name | | 'Unknown'. Wrapped.displayName = `withErrorBoundary(${name})`; return Wrapped; }Copy the code

It’s a little more concise to use:

// Business subcomponent const User = () => {return <div>User</div>} // Add a layer of ErrorBoundary const UserWithErrorBoundary = WithErrorBoundary (User, {onError: () => logger.error(' error '), onReset: () => console.log(' reset ')}) // Business parent const App = () => {return (<div> <UserWithErrorBoundary/> </div>)}Copy the code

You can also write withXXX as a decoration, and it’s also convenient to put @withXXX in the class Component, so I won’t expand it here.

Is there a better design? We observed that only “serious exceptions” were reported by the browser, such as TypeError: XXX is not a function mentioned at the beginning. JS is a dynamically typed language, and in the browser you can: NaN + 1, you can nan.tostring (), you can ‘1’ + 1 without any errors. ComponenDidCatch is not automatically caught for some errors:

However, the developers are aware of these errors in the code. Since the developers have access to these errors, the ErrorBoundary catch can be made by throwing the error directly:

  1. When there is an error, the developer calls handleError(error) to pass the error into the function itself;

  2. HandleError throws new Error(Error);

  3. ErrorBoundary finds an Error thrown from above and calls componentDidCatch to handle the Error;

  4. .

Here’s an example of how to implement React Hook:

/ function useErrorHandler<P=Error>(givenError? : P | null | undefined, ): React.Dispatch<React.SetStateAction<P | null>> { const [error, setError] = React.useState<P | null>(null); if (givenError) throw givenError; // Throw error if (error); // Return setError; // Return developer can manually set error hook}Copy the code

Using the hooks above, there are two ways to handle errors that need to be handled yourself:

  1. Const handleError = useErrorHandler(), then handleError(yourError);

  2. UseErrorHandler (otherHookError). If the hooks contain export error, pass this error to useErrorHandler.

Such as:

function Greeting() { const [greeting, setGreeting] = React.useState(null) const handleError = useErrorHandler() function handleSubmit(event) { event.preventDefault() const name = event.target.elements.name.value fetchGreeting(name).then( newGreeting => } return greeting? (<div>{greeting}</div>) : ( <form onSubmit={handleSubmit}> <label>Name</label> <input id="name" /> <button type="submit">get a greeting</button> </form>)} // wrap withErrorBoundary to handle manually thrown errors export default withErrorBoundary(Greeting)Copy the code

Or:

function Greeting() { const [name, setName] = React.useState('') const {greeting, Error} = useGreeting(name) UseErrorHandler (error) function handleSubmit(event) {event.preventDefault() const name = event.target.elements.name.value setName(name) } return greeting ? ( <div>{greeting}</div> ) : ( <form onSubmit={handleSubmit}> <label>Name</label> <input id="name" /> <button type="submit">get a greeting</button> </form>)} // wrap withErrorBoundary to handle manually thrown errors export default withErrorBoundary(Greeting)Copy the code

Conclusion:

  1. Provide withErrorBoundary method to wrap business components to achieve exception catching;

  2. Provide useErrorHandler hook to let developers handle/throw errors themselves.

conclusion

To recap the main points above:

  1. Create an ErrorBoundary wheel;

  2. ComponentDidCatch catch page error, getDerivedStateFromError updates ErrorBoundary state and gets specific error;

  3. Provides various entry points for displaying error content: fallback, FallbackComponent, fallbackRender;

  4. Reset hook: Provides onReset, resetErrorBoundary values and calls to reset

  5. Reset listener array: Listen for changes in resetKeys to reset. Provide onResetKeysChange for resetKeys arrays with complex elements for developers to decide for themselves. ComponentDidUpdate listens to the resetKeys change during each rendering, and sets updatedWithError as a flag to determine whether the rendering is caused by error. For ordinary rendering, as long as the resetKeys change, directly reset;

  6. Two ways of using ErrorBoundary are provided: nesting business components and passing business components into withErrorBoundary higher-order functions. Provide useErrorBoundary hooks for developers to throw errors that ErrorBoundary cannot automatically catch;

Rattail juice. Think about it

Finished this set of “five consecutive whip”, once again released online, all OK.

Later, I went to the back end and told him about the online incident. He was in tears, covering his face, and two minutes later, he was fine.

I said: young man, unless you speak code, you don’t understand. He said: SORRY, I don’t understand the rules. Then he said he’d been writing dynamic languages for years, and ah, looks like bear is coming. The young man does not speak code. Come on! Cheat! Come on! Sneak up on a 24-year-old of mine. Is that good? That’s not good, and I suggest, this back end, rattail juice, think about it, and don’t do that kind of smart, smart. Program ape to harmony, to speak code, do not engage in infighting. Thank you, friends!

(The story is pure fiction, please give a thumbs up if it is similar.)

All of the following code has been collated to Github:

Github.com/Haixiang612…

Reference wheels:

www.npmjs.com/package/rea…