This project is the front part of the stability project of cloud music revenue group. The author of this article is Zhang Weidong, and other participants of the project are Zhao Xiangtao

A bug led to a bloodbath

A famous male group in South Korea put a heavy digital album on our platform before, which was originally a good thing with great joy, but the complaints poured in after it was put on the shelves. Some user feedback page opened crash, emergency investigation found that the real culprit is the following code.

  render() {
     const { data, isCreator, canSignOut, canSignIn } = this.props;
     const {  supportCard, creator, fansList, visitorId, memberCount } = data;
     let getUserIcon = (obj) = > {
         if (obj.userType == 4) {
             return(<i className="icn u-svg u-svg-yyr_sml" />); } else if (obj.authStatus == 1) { return (<i className="icn u-svg u-svg-vip_sml" />); } else if (obj.expertTags && creator.expertTags.length > 0) { return (<i className="icn u-svg u-svg-daren_sml" />); } return null; }; . }Copy the code

This line if (obj. ExpertTags && creator. ExpertTags. Length) inside the creator should be obj, due to the fingers, accidentally write wrong.

In this case, the Lint tool cannot detect because Creator also happens to be a variable, which is a pure logical error.

We urgently fixed the bug and everything calmed down. This is the end of the story, but there is a voice in my heart how to avoid this kind of accident again. This kind of error cannot be blocked, so we should think about designing a backstop mechanism that can isolate the error and ensure that the whole page is not affected if a part of the page fails.

ErrorBoundary introduction

Starting with React 16, Error Boundaries were introduced. This concept allows you to catch errors in your subcomponents, log errors, and display the degraded content.

Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed

This trait brightens our eyes and lifts our spirits, as if we see a light in the darkness. However, after study, ErrorBoundary can only capture the render error of sub-components, which has certain limitations. The following are the cases that cannot be handled:

  • Event handlers (such as onClick,onMouseEnter)
  • Asynchronous code (e.g. RequestAnimationFrame, setTimeout, Promise)
  • Server side rendering
  • ErrorBoundary Error of the component itself.

How to create aErrorBoundarycomponent

Just add static getDerivedStateFromError() or componentDidCatch() to the React.component component. The former is used to degrade when errors occur, and the latter is mainly used for logging. The official code is as follows

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; }}Copy the code

You can see that getDerivedStateFromError catches errors in child components, sets the hasError variable, and displays the degraded UI in the Render function based on the value of the variable.

So far an ErrorBoundary component has been defined, just wrap a sub-component for use as follows.

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>
Copy the code

Common usage of Error Boundaries.

After seeing how Error Boundaries are used, most teams will follow the official usage of writing an errorBoundaryHOC and wrapping the components. Here’s an example of a Scratch project

export default errorBoundaryHOC('Blocks')(
    connect(
        mapStateToProps,
        mapDispatchToProps
    )(Blocks)
);
Copy the code

Blocks is a UI presentation component, and errorBoundaryHOC is an error handling component. See the source code here

The dilemma of universal usage

The above method wraps an errorBoundaryHOC when exporting. For the newly developed code, it is more convenient to use, but for the existing code, there will be a relatively big problem.

Because there are many formats of export

export class ClassName {... }export { name1, name2, …, nameN };
export { variable1 as name1, variable2 asName2,... , nameN };export * as name1 fromCopy the code

Therefore, if the original code is encapsulated with errorBoundaryHOC, the original code structure will be changed. If the subsequent encapsulation and deletion are no longer needed, it will also be troublesome, and the implementation cost of the scheme is high and very difficult.

Therefore, we are considering whether there is a more convenient way to deal with the above problems.

Bronze Age – BabelPlugin

After encountering the appeal dilemma, the idea was to automatically wrap the error handling component around the sub-component through scaffolding. The design framework is as follows:

In short, the following steps:

  1. Check whether the React 16 version is available

  2. Reading configuration Files

  3. Check whether the ErrorBoundary component has been wrapped. If no, go to the Patch process. If yes, determine whether to repackage the package according to the force label.

  4. Go through the package component flow (patch flow in the figure) :

    A. Introduce the error handling component first

    B. Wrap sub-components with ErrorBoundary

The configuration file is.catch-react-error-config.json:

{
  "sentinel": {
    "imports": "import ServerErrorBoundary from '$components/ServerErrorBoundary'"."errorHandleComponent": "ServerErrorBoundary"."filter": ["/actual/"]},"sourceDir": "test/fixtures/wrapCustomComponent"
}
Copy the code

Source code before Patch:

import React, { Component } from "react";

class App extends Component {
  render() {
    return <CustomComponent />; }}Copy the code

The code after reading the patch configuration file is:

//isCatchReactError
import ServerErrorBoundary from "$components/ServerErrorBoundary";
import React, { Component } from "react";

class App extends Component {
  render() {
    return (
      <ServerErrorBoundary isCatchReactError>
        {<CustomComponent />}
      </ServerErrorBoundary>); }}Copy the code

You can see more heads

import ServerErrorBoundary from '$components/ServerErrorBoundary'

Then the whole component is also wrapped by ServerErrorBoundary. IsCatchReactError is used to mark bits, mainly to make corresponding updates according to this bit during the next patch to prevent it from being introduced many times.

With the help of Babel Plugin, this scheme automatically imports ErrorBoundary and packages components in batches during code compilation. The core code is as follows:

const babelTemplate = require("@babel/template");
const t = require("babel-types");

const visitor = {
  Program: {
    // Import ErrorBoundary in the header of the file
    exit(path) {
      // String code is converted to AST
      const impstm = template.default.ast(
        "import ErrorBoundary from '$components/ErrorBoundary'"); path.node.body.unshift(impstm); }},Return jsxElement * @param {*} path */
  ReturnStatement(path) {
    const parentFunc = path.getFunctionParent();
    const oldJsx = path.node.argument;
    if(! oldJsx || ((! parentFunc.node.key || parentFunc.node.key.name ! = ="render") && oldJsx.type ! = ="JSXElement")) {return;
    }

    // Create the component tree wrapped by ErrorBoundary
    const openingElement = t.JSXOpeningElement(
      t.JSXIdentifier("ErrorBoundary"));const closingElement = t.JSXClosingElement(
      t.JSXIdentifier("ErrorBoundary"));const newJsx = t.JSXElement(openingElement, closingElement, oldJsx);

    // Insert new jxsElement and delete the old one
    letnewReturnStm = t.returnStatement(newJsx); path.remove(); path.parent.body.push(newReturnStm); }};Copy the code

The core of this solution is to wrap the sub-component with a custom component that happens to be ErrorBoundary. Custom components can also be other components such as logs, if desired.

Complete GitHub code implemented here

While this approach achieves the wrong capture and bottom-of-the-pocket approach, it is very complex and cumbersome to use, and requires scaffolding to configure Webpack and.catch-react-error-config.json, which is unsatisfactory.

Golden Age – Decorator

After the above scheme came out, it was difficult to find an elegant solution for a long time, either too difficult to use (babelplugin), or too much change to the source code (HOC), whether there is a more elegant implementation.

Here comes the Decorator scheme.

The source code for the decorator solution uses TypeScript, which needs to be converted to ES with the Babel plugin, as shown in the instructions below

TS provides a decorator factory, class decorator, method decorator, accessor decorator, attribute decorator, parameter decorator and so on. According to the characteristics of the project, we use class decorator.

Class decorator introduction

Class decorators are declared before the class declaration (right next to the class declaration). Class decorators are applied to class constructors and can be used to monitor, modify, or replace class definitions.

Here’s an example.

function SelfDriving(constructorFunction: Function) {
    console.log('-- decorator function invoked --');
    constructorFunction.prototype.selfDrivable = true;
}

@SelfDriving
class Car {
    private _make: string;
    constructor(make: string) {
        this._make = make; }}let car: Car = new Car("Nissan");
console.log(car);
console.log(`selfDriving: ${car['selfDrivable']}`);
Copy the code

output:

-- decorator function invoked --
Car { _make: 'Nissan' }
selfDriving: true
Copy the code

The code above first executes the SelfDriving function, and then car acquires the selfDrivable property.

You can see that a Decorator is essentially a function, but it can also be decorated with @+ function names in classes, methods, etc. Decorators can change class definitions, get dynamic data, and so on.

Please refer to the official tutorial Decorator for the complete TS tutorial Decorator

So our error capture scheme is designed as follows

@catchreacterror()
class Test extends React.Component {
  render() {
    return <Button text="click me" />; }}Copy the code

Catchreacterror takes an ErrorBoundary component as an argument. Users can use a custom ErrorBoundary or use a default DefaultErrorBoundary component if they do not pass it.

Catchreacterror catchreacterror

import React, { Component, forwardRef } from "react";

const catchreacterror = (Boundary = DefaultErrorBoundary) = > InnerComponent => {
  class WrapperComponent extends Component {
    render() {
      const { forwardedRef } = this.props;
      return (
        <Boundary>
          <InnerComponent {. this.props} ref={forwardedRef} />
        </Boundary>); }}};Copy the code

The return value is a HOC, wrapped with the child component using ErrorBoundary.

Added server-side rendering error capture

It was mentioned in the introduction that the official ErrorBoundary did not support server-side rendering, so we used try/catch to make parcel for SSR:

  1. Check whether it is a serveris_server:
function is_server() {
  return! (typeof window! = ="undefined" && window.document);
}
Copy the code
  1. The parcel
if (is_server()) {
  const originalRender = InnerComponent.prototype.render;

  InnerComponent.prototype.render = function() {
    try {
      return originalRender.apply(this.arguments);
    } catch (error) {
      console.error(error);
      return <div>Something is Wrong</div>; }}; }Copy the code

Finally, we created a catch-react-error library to catch react errors.

Catch-react-error usage instructions

1. Installcatch-react-error

npm install catch-react-error
Copy the code

2. Install ES7 Decorator Babel plugin

npm install --save-dev @babel/plugin-proposal-decorators
npm install --save-dev @babel/plugin-proposal-class-properties

Copy the code

Add the Babel plugin

{
  "plugins": [["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", { "loose": true}}]]Copy the code

3. Import catch – the react – the error

import catchreacterror from "catch-react-error";
Copy the code

Use 4.@catchreacterror Decorator

@catchreacterror()
class Test extends React.Component {
  render() {
    return <Button text="click me" />; }}Copy the code

The catchreacterror function accepts one argument: ErrorBoundary (default defaultterrorBoundary if not provided)

5. Use@catchreacterrorProcessing FunctionComponent

This is for ClassComponent, but some people prefer to use function components. Here’s how to use them, too.

const Content = (props, b, c) = > {
  return <div>{props.x.length}</div>;
};

const SafeContent = catchreacterror(DefaultErrorBoundary)(Content);

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <h1>This is the normal display</h1>
      </header>
      <SafeContent/>
    </div>
  );
}

Copy the code

6. How do I create Custom Error Boundaries

Refer to how to create an ErrorBoundary component above and then change it to what you need, such as reporting errors in componentDidCatch, etc.

The full GitHub code is here for catch-react-error.

This article is published from netease Cloud music front end team, the article is prohibited to be reproduced in any form without authorization. We’re always looking for people, so if you’re ready to change jobs and you like cloud music, join us!