[React] 使用 Error Boundary 作為子組件渲染異常的破壞邊界

在 React 16 版本後新增錯誤邊界 (Error Boundary) 的概念,新加入 componentDidCatch 組件生命週期方法來捕捉「子組件」錯誤,當邊界內層子組件發生不預期錯誤時,不會影響到邊界外層父組件;當然也會有一些限制存在,本篇文章來針對 Error Boundary 進行說明。

前言


以往對於組件生成的錯誤處理多為 try catch 方式,但在組件生成的過程會經歷了許多生命週期方法,若想要全面性處理異常錯誤,往往帶來的負面效果就是代碼充斥著錯誤捕捉的結構,沒有一個比較恰當的方式來處理;但在 React 16 發布後新增了 componentDidCatch 組件生命週期方法,讓開發人員可以於此統一處理子組件的異常錯誤。以下介紹。

 

 

錯誤邊界 (Error Boundary)


想要成為一個 Error Boundary 的方式相當簡單,只要在組件中加入 componentDidCatch 方法就成為 Error Boundary 組件,當「子組件」發生錯誤時就會被這個 Error Boundary 捕捉,不會讓錯誤影響到錯誤邊界外的任何組件;另外,開發人員可透過 error 與 info 參數記錄錯誤的相關資訊,對於錯誤釐清有很大的幫助。

class ErrorBoundary extends React.Component {

  // ...

  componentDidCatch (error, info) {

    // 子組件發生錯誤!!

    // 可透過 state 控制錯誤發生的畫面呈現樣貌
    this.setState({ hasError: true })
    
    // 可利用 error 與 info 記錄下錯誤資訊
    console.error(error, info)
  }

  // ...

}

 

 

限制項目


請特別注意 Error Boundary 並不是所有錯誤都會被捕捉,相關限制及特性如下:

  1. 只能捕捉「子」組件錯誤,邊界本身錯誤無法被捕捉。
  2. 只能捕捉子組件「渲染」、「建構子」及「各生命週期方法」中的錯誤。
  3. 牽涉到非同步執行 (ex. setTimeout, callback ) 中的錯誤無法被捕捉。
  4. 無法捕捉 event handler 中發生的錯誤。
仍需要搭配 try catch 來 handle 無法被 Error Boundary 捕捉的錯誤。

 

 

實作驗證


驗證首先目標就是建立出用來發出錯誤的炸彈組件,接著依照官方文件建立 Error Boundary 並以此區隔這些炸彈組件,以此理解當錯誤時的處置方式。

 

炸彈組件

首先建立以產生渲染錯誤的 TNTBomp 組件,當點選「爆炸」按鈕後隨即拋出錯誤,後續將以此來測試錯誤發生時能否被 Error Boundary 所捕捉;另外,產生 Box 組件來模擬一般組件,就直接在頁面輸出色塊樣式囉。

import React from 'react'
import styled from 'styled-components'

class TNTBomp extends React.Component {
  constructor (props) {
    super(props)
    this.state = { isExplode: false }
  }

  handleClick = () => {
    // 從這邊拋出去的錯誤不會被 ErrorBoundary 捕捉
    // 請使用 try catch 來處理這類錯誤
    this.setState(state => ({ ...state, isExplode: true }))
  }

  render () {
    if (this.state.isExplode) {
      throw new Error('I crashed!')
    }
    return <button type='button' className='btn btn-danger' onClick={this.handleClick}>
      爆炸!!
    </button>
  }
}

const Box = styled.div`
  padding: 5px 5px 5px 5px;
  margin-bottom: 5px;
  border: solid;
  border-width: 1px;
  background:  ${props => props.bkColor};
`


export default () => {
  return <React.Fragment>
    {/* React 16.2 新增 React.Fragment 空白標籤 */}

    <h2>Error Boundaries</h2>
    <hr />

    {/* bomp without error boundary */}
    <Box bkColor='#afd3ff'>
      <TNTBomp />
    </Box>

  </React.Fragment>
}

 

執行後畫面上就會出現一顆 TNTBomp 炸彈埋在藍色 Box 中。

 

引爆後發現整頁變空白,這樣的用戶體驗也太慘烈了;主要是因為在 React 16 後為了保持頁面完整性,避免組件錯誤後繼續操作而造成邏輯上的麻煩,因此若組件生成錯誤沒有被任何 Error Boundary 捕捉,就會將整個 component tree 卸掉變成空白頁。

建議在 root 組件補上 componentDidCatch 方法,並產生錯誤提示來做為網站的最後一道防線。

 

 

使用 Error Boundary 包裹炸彈

直接建立一個 ErrorBoundary 組件,當錯誤發生時會在頁面顯示「被 ErrorBoundary 擋住錯誤」文字,並且將剛剛產生的炸彈給包裹起來;另外,為了在畫面能比較清楚地呈現,就給他個紅色虛線 border 樣式來作標記。

// ...

const ErrorBoundaryBox = styled.div`
  padding: 5px 5px 0px 5px;
  margin-bottom: 5px;
  border: dashed;
  border-color: coral;
  border-width: 3px;
  background:  ${props => props.bkColor};
`

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

  componentDidCatch (error, info) {
    // Display fallback UI
    this.setState({ hasError: true })
    // You can also log the error to an error reporting service
    console.error(error, info)
  }

  render () {
    return <ErrorBoundaryBox>
      {this.state.hasError
        ? <h4>被 ErrorBoundary 擋住錯誤啦!!</h4>
        : this.props.children}
    </ErrorBoundaryBox>
  }
}


export default () => {
  return <React.Fragment>
    {/* React 16.2 新增 React.Fragment 空白標籤 */}

    <h2>Error Boundaries</h2>
    <hr />

    {/* bomp with error boundary */}
    <ErrorBoundary>
      <Box bkColor='#afd3ff'>
        <TNTBomp />
      </Box>
    </ErrorBoundary>

  </React.Fragment>
}

 

輸出在畫面上後,紅色虛線的部分就是 Error Boundary 組件區塊,內含一個炸彈組件。

 

引爆後錯誤就被 Error Boundary 所捕捉,不再影響整體網站的顯示。

 

 

巢狀 Error Boundary

接著來觀察一下 Error Boundary 作用域,以下定義一些巢狀 Error Boundary 來包裹炸彈。

// ... 


export default () => {
  return <React.Fragment>
    {/* React.Fragment 為 React 16.2 新增 React.Fragment 空白標籤 */}

    <h2>Error Boundaries</h2>
    <hr />

    {/* bomp in error boundary */}
    <ErrorBoundary>
      <Box bkColor='#afd3ff'>
        <TNTBomp />
      </Box>
      <Box bkColor='#afd3ff'>
        <TNTBomp />
      </Box>

      {/* catch by the first parent error boundary */}
      <ErrorBoundary>
        <Box bkColor='#afd3ff'>
          <TNTBomp />
        </Box>
      </ErrorBoundary>

      {/* bomp in nested components still can be catched */}
      <ErrorBoundary>
        <Box bkColor='#afd3ff'>
          <Box bkColor='#c1f8aa'>
            <TNTBomp />
          </Box>
        </Box>
      </ErrorBoundary>

    </ErrorBoundary>
  </React.Fragment>
}

 

從畫面上可以看到炸彈在不同組件中,並且被各自 Error Boundary 所包裹起來。

 

各自引爆後可以發現錯誤會被最接近自己的 Error Boundary 給捕捉,因此可以利用這特性依據組件顆粒度大小來制定合適的錯誤邊界;例如頁面中若同時需要顯示許多獨立性的資訊區塊,我們就可以為這些獨立區塊建立各自的 Error Boundary 組件,這樣就算發生錯誤也不會影響到彼此資訊的呈現。

 

 

參考資訊


React - Error Boundaries

React - Error Handling in React 16

React新特性——Protals与Error Boundaries

 

測試代碼已上傳 GitHub 中,有興趣的朋友可以參考一下。
若有更好的建議或做法再請不吝指導一下囉! 感謝!

 


希望此篇文章可以幫助到需要的人

若內容有誤或有其他建議請不吝留言給筆者喔 !