错误边界(Error Boundaries)

过去,组件内的 JavaScript 错误常常会破坏 React 内部状态,并导致它在下一次渲染时产生 神秘的 错误。这些错误总会在应用代码中较早的错误引发的,但 React 并没有提供一种方式能够在组件内部优雅地来处理,也不能从错误中恢复。

错误边界介绍

部分 UI 中的 JavaScript 错误不应该破坏整个应用程序。 为了解决 React 用户的这个问题,React 16引入了一个 “错误边界(Error Boundaries)” 的新概念。

错误边界是 React 组件,它可以 在子组件树的任何位置捕获 JavaScript 错误,记录这些错误,并显示一个备用 UI ,而不是使整个组件树崩溃。 错误边界(Error Boundaries) 在渲染,生命周期方法以及整个组件树下的构造函数中捕获错误。

注意

错误边界 无法 捕获如下错误:

  • 事件处理 (了解更多
  • 异步代码 (例如 setTimeoutrequestAnimationFrame 回调函数)
  • 服务端渲染
  • 错误边界自身抛出来的错误 (而不是其子组件)

如果一个类组件定义了生命周期方法中的任何一个(或两个)static getDerivedStateFromError()componentDidCatch(),那么它就成了一个错误边界。 使用static getDerivedStateFromError()在抛出错误后渲染回退UI。 使用 componentDidCatch() 来记录错误信息。

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, info) {
    // You can also log the error to an error reporting service
    logErrorToMyService(error, info);
  }

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

    return this.props.children; 
  }
}

而后你可以像一个普通的组件一样使用:

```js
<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

错误边界的工作方式类似于JavaScript catch {} 块,但是针对组件。 仅有类组件可以成为错误边界。 实际上,大多数情况下,您需要声明一次错误边界组件并在整个应用程序中使用它。

注意 错误边界(Error Boundaries) 仅可以捕获其子组件的错误。错误边界无法捕获其自身的错误。如果一个错误边界无法渲染错误信息,则错误会向上冒泡至最接近的错误边界。这也类似于 JavaScript 中 catch {} 的工作机制。

在线演示

查看通过 React 16定义和使用错误边界的例子

如何放置错误边界

错误边界的粒度完全取决于你的应用。你可以将其包装在最顶层的路由组件并为用户展示一个 “发生异常(Something went wrong)“的错误信息,就像服务端框架通常处理崩溃一样。你也可以将单独的插件包装在错误边界内部以保护应用不受该组件崩溃的影响。

未捕获错误(Uncaught Errors)的新行为

这一改变有非常重要的意义。自 React 16 开始,任何未被错误边界捕获的错误将会卸载整个 React 组件树。

我们对这一决定饱含争论,但在我们的经验中放置下一个错误的UI比完全移除它要更糟糕。例如,在类似 Messenger 的产品中留下一个异常的可见 UI 可能会导致用户将信息发错给别人。类似的,对于支付类的应用来说,什么都不展示也比显示一堆错误更好。

这一改变意味着随着你迁入到 React 16,你将可能会发现一些已存在你应用中但未曾注意到的崩溃。增加错误边界能够让你在发生异常时提供更好的用户体验。

例如,Facebook Messenger 将侧边栏、信息面板,对话框以及信息输入框包装在单独的错误边界中。如果其中的某些 UI 组件崩溃,其余部分仍然能够交互。

我们也鼓励使用 JS 错误报告服务(或自行构建)这样你能够掌握在生产环境中发生的未捕获的异常,并将其修复。

组件栈追踪

React 16 会将渲染期间所有在开发环境下的发生的错误打印到控制台,即使应用程序意外的将其掩盖。除了错误信息和 JavaScript 栈外,其还提供了组件栈追踪。现在你可以准确地查看发生在组件树内的错误信息:

Error caught by Error Boundary component

你也可以在组件堆栈中查看文件名和行数。这一功能在 Create React App 项目中默认开启:

Error caught by Error Boundary component with line numbers

若你不使用 Create React App,你可以手动添加该插件到你的 Babel 配置中。注意其仅能在开发环境中使用并 禁止在生产环境中使用

注意

显示在堆栈跟踪中的组件名称取决于Function.name 属性。如果你想要支持尚未提供此功能的旧版浏览器和设备(例如 IE 11),考虑在你的 打包(bundled) 应用程序中包含一个 Function.name 的polyfill,如 function.name-polyfill 。或者,你可以在所有组件上显式设置 displayName 属性。

为何不使用 try/catch?

try / catch 非常棒,但其仅能在命令式代码(imperative code)下可用:

try {
  showButton();
} catch (error) {
  // ...
}

然而,React 组件是声明式的并且具体指出 声明 什么需要被渲染:

<Button />

错误边界保留了 React 原生的声明性质,且其行为符合你的预期。例如,即使错误发生 componentDidUpdate 时期由某一个深层组件树中的 setState 调用引起,其仍然能够冒泡到最近的错误边界。

事件处理器如何处理?

错误边界 无法 捕获事件处理器内部的错误。

React 不需要错误边界在事件处理器内将其从错误中恢复。不像渲染方法或生命周期方法,事件处理器不会再渲染周期内触发。因此若他们抛出异常,React 仍然能够知道需要在屏幕上显示什么。

如果你需要在事件处理器内部捕获错误,使用普通的 JavaScript try / catch 语句:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    try {
      // Do something that could throw
    } catch (error) {
      this.setState({ error });
    }
  }

  render() {
    if (this.state.error) {
      return <h1>Caught an error.</h1>
    }
    return <div onClick={this.handleClick}>Click Me</div>
  }
}

注意上述例子仅是说明普通的 JavaScript 行为而并未使用错误边界。

自 React 15 的名称变更

React 15 在一个不同的方法名下:unstable_handleError 包含了一个支持有限的错误边界。这一方法不再能用,同时自 React 16 beta 发布起你需要在代码中将其修改为 componentDidCatch

为这一改变,我们已提供了一个 codemod 来帮助你自动迁移你的代码。