React 錯誤邊界元件

xuweiblog發表於2021-04-11

這是React16的內容,並不是最新的技術,但是用很少被討論,直到通過文件發現其實也是很有用的一部分內容,還是總結一下~

React中的未捕獲的 JS 錯誤會導致整個應用的崩潰,和整個元件樹的解除安裝。從 React16 開始就是這樣。但是同時React也引入了一個新的概念——錯誤邊界。

定義,是什麼

錯誤邊界仍然是一種元件,可以捕獲(列印或者其他方式)處理該元件的子元件樹任何位置的 JavaScript 錯誤,並根據需要渲染出備用UI.

工作方式類似於try-catch,但是錯誤邊界只用於 React 元件。

只有class元件能夠成為錯誤邊界元件。錯誤邊界僅可以捕獲子元件的錯誤,無法捕獲自身的錯誤

錯誤邊界會在渲染期間,生命週期和整個元件樹的建構函式中捕獲錯誤。如果沒有錯誤邊界處理,渲染的還是崩潰的子元件樹,這顯然不是我們想要的。

通過一個例子來逐步演示要怎麼用錯誤邊界:

export default class ErrorTest extends Component {
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <div>
        <BugCounter></BugCounter>
        <span>my name is dan</span>
      </div>
    );
  }
}

// Bug 報錯元件
class BugCounter extends Component {
  constructor(props) {
    super(props);
    this.state = {
      counter: 0,
    };
  }
  click = () => {
    this.setState(({ counter }) => ({ counter: counter + 1 }));
  };
  render() {
    if (this.state.counter === 5) {
      throw new Error("crashed!");
    }
    return (
      <div>
        <h3 onClick={this.click}>{this.state.counter}</h3>
      </div>
    );
  }
}

上面程式碼的渲染結果(忽略樣式):

React 錯誤邊界元件

點選數字0,會逐步遞增。但是數字等於5的時候,元件會丟擲一個Error:

React 錯誤邊界元件

Error會引起整個Demo的崩潰,連外部的<span>my name is dan</span>也顯示不出來了,這時還沒有新增錯誤邊界。

生產模式下,會直接白屏,並在控制檯報錯:

React 錯誤邊界元件

getDerivedStateFromError & componentDidCatch

需要一個錯誤邊界來處理這種崩潰。如何定義一個錯誤邊界?

定義一個元件,並實現static getDerivedStateFromError() 或者componentDidCatch() 生命週期方法(可以都實現或者選擇其一)。這個元件就會變成一個錯誤邊界

關於這兩個生命週期函式,可以通過連結檢視,總結如下:

componentDidCatch(error, info)

error是丟擲的錯誤物件,而info則包含了元件引發錯誤的棧資訊。函式在提交階段被呼叫。是可以執行副作用的。

static getDerivedStateFromError(error)

在子元件丟擲錯誤後呼叫,會將丟擲的錯誤作為引數。需要返回一個值,以更新state。該函式在渲染階段呼叫,不允許出現副作用。如果在捕獲錯誤後需要執行副作用操作,應該在componentDidCatch中進行。

製作錯誤邊界元件

可以使用組合的方式,在要使用的元件上面新增一個錯誤邊界元件包裹一層。該元件需要這些效果:

  • 捕獲子元件錯誤,元件內部記錄出錯狀態
  • 在出錯狀態下顯示備用UI,在正常狀態下顯示子元件

那麼就可以像這樣:

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

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能夠顯示降級後的 UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 你同樣可以將錯誤日誌上報給伺服器
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 你可以自定義降級後的 UI 並渲染
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}

捕獲到錯誤之後的副作用是自定義的,上傳伺服器,或者用state記錄再顯示在頁面上:

componentDidCatch(error, errorInfo) {
  // Catch errors in any components below and re-render with error message
  this.setState({
    error: error,
    errorInfo: errorInfo
  })
}

捕獲處理

加上所有程式碼,將有問題的元件用錯誤邊界的元件包裹起來,看看結果:

import { Component } from "react";

export default class ErrorTest extends Component {
  render() {
    return (
      <div>
        <ErrorBoundary>
          <BugCounter></BugCounter>
        </ErrorBoundary>
        <span>my name is dan</span>
      </div>
    );
  }
}

// Bug 報錯元件
class BugCounter extends Component {
  constructor(props) {
    super(props);
    this.state = {
      counter: 0,
    };
  }
  click = () => {
    this.setState(({ counter }) => ({ counter: counter + 1 }));
  };
  render() {
    if (this.state.counter === 5) {
      throw new Error("crashed!");
    }
    return (
      <div>
        <h3 onClick={this.click}>{this.state.counter}</h3>
      </div>
    );
  }
}

// 錯誤邊界處理元件
class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能夠顯示降級後的 UI
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      // 你可以自定義降級後的 UI 並渲染
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

丟擲異常在開發模式下依然是報錯的,但是在使用yarn build之後,再通過http-server掛起來之後,訪問生產的頁面:

React 錯誤邊界元件

可以看到,雖然因為throw error控制檯出錯,但是my name is dan的顯示並沒有被影響,也就是說,錯誤邊界內部的子元件錯誤沒有影響到外部其他元件和元素。

作用範圍

錯誤邊界用於處理子元件生命週期和渲染函式上的錯誤,對於事件處理器,不會在渲染期間觸發,對於事件處理器丟擲的異常應該用try catch

錯誤邊界無法捕獲這些場景中的錯誤:

  • 事件處理
  • 非同步程式碼
  • 服務端渲染
  • 錯誤邊界自身丟擲的錯誤(非子元件)

關於錯誤邊界,一個 React的官方demo值得嘗試:

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


參考:

相關文章