[铁人赛 Day06] React 中如何拦截网站 Runtime 错误?- Error boundaries

前言

前一篇 Code Spliting 文章中有提到用 Error boundaries 来处理载入错误的显示,实际上该如何实作呢?本篇会实际动手尝试,给各位参考。

Error boundaries 是什麽?

一句话来说:Error boundaries 可以捕捉 React 元件内发生的 JavaScript 错误,并展示出错误讯息。

Error boundaries 解决什麽问题?

React 网站在运行时,可能会因为元件内部的 JavaScript 错误,干扰原本的 state 甚至导致下一次的 render 含有隐藏的错误(emit cryptic errors),这些眼前的错误可能来自於更早发生的错误,让人头大。React (以往)并没有提供一个方法去处理这种情境,导致网站会无法恢复原状。

一个 UI 上的错误,不应该让整个网站受到影响,因此 React 16 推出了 Error boundaries 这个新概念。Error boundaries 可以在 rendering 的过程中(在生命周期、constructors 内)「抓住」JavaScript 错误,并且印出错误讯息、也可以自行设定 fallback 的 UI。

不适用 Error boundaries 的情境

Error boundaries 不会抓住以下的错误:

  1. Event handlers(需要使用 try catch)
  2. 非同步的运作(例如:setTimeout)
  3. SSR(Server side rendering)
  4. error boundary 本身发生的错误

以及,因为 error boundary 必须使用 class component,所以无法用 hook 来写。

实作 Error boundaries

  1. 模拟情境:

画面上、下半部各有一些按钮,点击下半部的按钮之後,刻意制造一个错误,让下半部页面显示错误讯息,但上半部不会被影响。

  1. 等等会用到的 API reference & HTML element:

用 throw statement 来丢出一个自定义的意外情境,正在执行的函式会停止,而写在 throw 之後的陈述也不会触发。

throw new Error('Required'); 
// 这里会制造出一个带有错误讯息的 error 物件,稍後可以使用 Error boundaries 印出讯息

错误讯息可以使用 <details> 元素来展示,<details> 会建立出一个可以收合的讯息元件。

<details>
    <summary>标题</summary>
    讯息展开之後的内文
</details>

用 componentDidCatch() 来印出错误讯息,会接收两个参数 error & info

error - 代表程序出错的地方丢回来的 error
info - 可以取得做 Component Stack Traces 相关资讯的物件,可以用来追到实际上到底是哪一个物件出错

实作

// 建立一个一定会坏掉的按钮
class BreakButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick = () => {
    this.setState({
      hasError: true
    })
  }
  render() {
    if (this.state.hasError) {
      throw new Error('这是错误讯息');
    }
    return <button onClick={this.handleClick}>Click</button>;
  }
}

// 画面结构上下各有一颗按钮
const ErrorExample = () => {
  return (
    <div>
        <p>这个按钮会跳出警告</p>
        <button onClick={() => { alert('这是警告') }}>
					Show Alert Button
				</button>
      <hr />
        <p>这个按钮会坏掉</p>
        <BreakButton />
    </div>
  );
}

在加入 error boundary 之前,看起来是这样(以下使用 codePen):

pic01

按下坏掉的按钮之後,整个页面都会被错误卡住。

让我们来加上 error boundary:

class component 只要包含 static getDerivedStateFromError() 或 componentDidCatch() 任一种方法就会变成 error boundary,以下会使用 componentDidCatch()。

// 注意:要使用 class component,hook 没有 error boundary
class CustomErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null, info: null };
  }
	// 用 componentDidCatch 来抓住错误
  componentDidCatch(error, info) {
    this.setState({
      error: error,
      info: info
    })
  }
  render() {
    if (this.state.error) {
      return (
        <>
          <h1>Oops!</h1>
          <details>
            {this.state.error && this.state.error.toString()}
            <br />
            {this.state.info.componentStack}
          </details>
        </>
      )
    }
    return this.props.children;
  }
}

并且在原本的画面上,加入 Error Boundary 元件,Error Boundary 会抓住子元件以下的 component tree 里头,所发生的错误。

const ErrorExample = () => {
  return (
    <div>
			// 这里上下半部各自用 Error Boundary 包起来
			// 上半部的运作就不会被下半部的错误影响
      <CustomErrorBoundary>
        <p>这个按钮会跳出警告</p>
        <button onClick={() => { alert('这是警告') }}>
					Show Alert Button
				</button>
      </CustomErrorBoundary>
      <hr />
      <CustomErrorBoundary>
        <p>这个按钮会坏掉</p>
        <BreakButton />
       </CustomErrorBoundary>
    </div>
  );
}

制作完成!以下是加上 Error Boundary 之後的效果:

pic02

可以看到错误讯息里,会标注实际产生错误的元件名称,并且,在下半部画面错误而无法继续运作的时候,画面上半部的按钮不会被影响。

===

官方文件

https://reactjs.org/docs/error-boundaries.html

Hook 不支援

https://stackoverflow.com/questions/60537645/how-to-implement-error-boundary-with-react-hooks-component

API reference

https://reactjs.org/docs/react-component.html#componentdidcatch

Details tag

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details

throw new error

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/throw

Sample by Dan

https://codepen.io/gaearon/pen/wqvxGa?editors=0010


<<:  Day21:[排序演算法]Heap Sort - 堆积排序法

>>:  D07 - 听话,给我资料!

#15 JS: if else statement

To make the operators meaningful for users, let’s ...

Day-5 注解与断行

一开始写code 最常使用的就是注解与断行,着解释为了方便标记説明想法,断行是为了更好阅读。 注解 ...

连续 30 天 玩玩看 ProtoPie - Day 28

Chain 和 Range 的差异 讲者很用心提到这个地方,他用换页的时後底下点点跟着移动作为例子。...

[Day 30] Android in Kotlin: 完赛心得

完赛罗 baby 这是我第一次参加铁人赛,这一个月下来,不仅算是给予暑假两个月的我一个复习的机会、加...

虹语岚访仲夏夜-24(专业的小四篇)

我慢慢的告诉小路,我记得的事,包括那个R... 小路一直强调,只有我一个人参加,然後,不会有什麽NP...