As of React 16, any error not caught by an error boundary will cause the entire React component tree to be uninstalled.

For large projects that have been in development for a long time and where Code Review is not as stringent, upgrading to React 16 may reveal that exceptions that were previously only occasionally localized and did not receive enough attention may now occasionally cause the entire application to collapse.

React 16 introduces Error Boundaries to address this situation.

Basic usage of Error Boundaries

If a class component defines any or all of the following lifecycle methods, the class component will become an error boundary.

  • static getDerivedStateFromError(error)

This static method is called during the render phase when a child component throws an error, and the received parameter err is the error thrown by the child component. The return value of the method is the updated state of the component for the render method to render the alternate UI. example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  render() {
    if (this.state.hasError) {
      return <h1>出错了</h1>;
    }
    return this.props.children;
  }
}
  • componentDidCatch(error, info)

Called when a child component throws an error, it takes two main parameters.

error : the error message thrown info : an object with the component error stack, containing information about which component threw the error

This lifecycle method is called during the commit phase and therefore allows for side effects. Example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  // static getDerivedStateFromError(error) {
  //   return { hasError: true };
  // }
  componentDidCatch(error, info) {
    this.setState({hasError: true});
    console.error(error, info.componentStack);
  }
  render() {
    if (this.state.hasError) {
      return <h1>出错了</h1>;
    }
    return this.props.children;
  }
}

Caution:

  • Error bounds can only catch errors of child components in the tree, not the component itself
  • Error boundaries can only be used to catch or recover from exceptions
  • Error bounds cannot be used to control processes

Define global default Error Boundaries handling functions

Considering the robustness of the entire application, designing Error Boundaries policies for each major component is necessary after React 16.

Define the HOC component for Error Boundaries processing

A simple way to do this is to design a HOC component and then apply it to the business component to be wrapped using the decorator pattern.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export default tip => EBWrapComponent => {
  return class ErrorBoundary extends React.Component{
    constructor(props) {
      super(props);
      this.state = { hasError: false };
    }
    static getDerivedStateFromError(error) {
      return { hasError: true };
    }
    componentDidCatch(error, info) {
      console.error(error, info.componentStack);
    }
    render() {
        if (this.state.hasError) {
          if (!tip) return;
          return <h2>{tip}</h2>;
        }
 
        return <EBWrapComponent />;
    }
  }
}

Application examples.

1
2
3
4
@ErrorBoundary('i am not ok')
export default class LzwmeTestComponent extends React.Component {
  // ...
}

Global compatibility: defining the default componentDidCatch method

For large applications with hundreds or thousands of components that have been developed for a long time, adding error boundary logic to the main components can still be cumbersome, and setting it only for the main module components can make local component exceptions magnified throughout the module.

A simple way to handle this would be to define a default lifecycle method componentDidCatch for React.Component

1
2
3
4
5
if (!React.Component.prototype.componentDidCatch) {
    React.Component.prototype.componentDidCatch = (error, info) => {
      return Sentry.captureException(error, null, info);
    }
}

Component, all components inherit the default lifecycle method componentDidCatch, so components with rendering exceptions are intercepted and handled in their parent component. Of course React will still throw a Warning warning for not updating the UI in the componentDidCatch method itself or defining the static getDerivedStateFromError method, but at least it won’t cause the whole app to be uninstalled.

Of course, the bottom line is that we should not rely only on the regular format of input data, and we should require component development to take exceptions and handling into account as much as possible to ensure the robustness of the component itself.