React 錯誤邊界 (Error Boundary)

錯誤邊界是 React 元件,可以捕捉子元件樹中的 JavaScript 錯誤,記錄這些錯誤,並顯示一個備用 UI,而不是讓整個應用程式崩潰。

為什麼需要錯誤邊界?

在沒有錯誤邊界的情況下,如果元件中發生 JavaScript 錯誤,整個 React 應用程式會崩潰,顯示空白頁面。錯誤邊界讓你可以優雅地處理錯誤,只讓出錯的部分顯示備用 UI,其他部分仍然正常運作。

建立錯誤邊界

錯誤邊界必須使用 Class Component,因為它依賴兩個生命週期方法:

import { Component } from 'react'

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

  // 當子元件拋出錯誤時,更新 state
  static getDerivedStateFromError(error) {
    return { hasError: true, error }
  }

  // 記錄錯誤資訊
  componentDidCatch(error, errorInfo) {
    console.error('錯誤:', error)
    console.error('錯誤資訊:', errorInfo.componentStack)

    // 可以將錯誤發送到錯誤追蹤服務
    // logErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 顯示備用 UI
      return (
        <div className="error-fallback">
          <h2>發生錯誤</h2>
          <p>很抱歉,發生了一些問題。</p>
          <button onClick={() => this.setState({ hasError: false, error: null })}>重試</button>
        </div>
      )
    }

    return this.props.children
  }
}

使用錯誤邊界

將可能發生錯誤的元件包裹在錯誤邊界中:

function App() {
  return (
    <div>
      <Header />

      <ErrorBoundary>
        <MainContent />
      </ErrorBoundary>

      <Footer />
    </div>
  )
}

這樣如果 MainContent 發生錯誤,只有該區塊會顯示備用 UI,HeaderFooter 仍然正常顯示。

多個錯誤邊界

你可以使用多個錯誤邊界來隔離不同區域的錯誤:

function Dashboard() {
  return (
    <div className="dashboard">
      <ErrorBoundary>
        <UserProfile />
      </ErrorBoundary>

      <ErrorBoundary>
        <ActivityFeed />
      </ErrorBoundary>

      <ErrorBoundary>
        <Recommendations />
      </ErrorBoundary>
    </div>
  )
}

每個區塊的錯誤會被獨立處理,不會影響其他區塊。

使用 react-error-boundary 函式庫

社群提供了 react-error-boundary 函式庫,讓你更方便地使用錯誤邊界:

npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary'

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div className="error-fallback">
      <h2>發生錯誤</h2>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>重試</button>
    </div>
  )
}

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => {
        // 重置應用程式狀態
      }}
      onError={(error, info) => {
        // 記錄錯誤
        console.error(error, info)
      }}
    >
      <MainContent />
    </ErrorBoundary>
  )
}

使用 fallbackRender

<ErrorBoundary
  fallbackRender={({ error, resetErrorBoundary }) => (
    <div>
      <p>錯誤:{error.message}</p>
      <button onClick={resetErrorBoundary}>重試</button>
    </div>
  )}
>
  <MyComponent />
</ErrorBoundary>

使用 useErrorBoundary Hook

import { useErrorBoundary } from 'react-error-boundary'

function MyComponent() {
  const { showBoundary } = useErrorBoundary()

  async function handleClick() {
    try {
      await riskyOperation()
    } catch (error) {
      // 手動觸發錯誤邊界
      showBoundary(error)
    }
  }

  return <button onClick={handleClick}>執行操作</button>
}

錯誤邊界的限制

錯誤邊界無法捕捉以下類型的錯誤:

  1. 事件處理函式中的錯誤

    function Button() {
      function handleClick() {
        throw new Error('錯誤') // 錯誤邊界無法捕捉
      }
      return <button onClick={handleClick}>Click</button>
    }
    

    解決方案:在事件處理函式中使用 try-catch

    function handleClick() {
      try {
        riskyOperation()
      } catch (error) {
        // 處理錯誤
        setError(error)
      }
    }
    
  2. 非同步程式碼

    useEffect(() => {
      fetch('/api').catch((error) => {
        // 錯誤邊界無法捕捉
      })
    }, [])
    
  3. 伺服器端渲染 (SSR)

  4. 錯誤邊界本身的錯誤

搭配 Suspense 使用

錯誤邊界和 Suspense 經常一起使用:

import { Suspense } from 'react'
import { ErrorBoundary } from 'react-error-boundary'

function App() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <Suspense fallback={<Loading />}>
        <AsyncComponent />
      </Suspense>
    </ErrorBoundary>
  )
}

完整範例:帶有重試功能的錯誤邊界

import { Component } from 'react'

class ErrorBoundary extends Component {
  constructor(props) {
    super(props)
    this.state = {
      hasError: false,
      error: null,
      errorInfo: null,
    }
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error }
  }

  componentDidCatch(error, errorInfo) {
    this.setState({ errorInfo })

    // 發送錯誤到監控服務
    if (this.props.onError) {
      this.props.onError(error, errorInfo)
    }
  }

  handleReset = () => {
    this.setState({ hasError: false, error: null, errorInfo: null })

    if (this.props.onReset) {
      this.props.onReset()
    }
  }

  render() {
    if (this.state.hasError) {
      // 自訂的 fallback UI
      if (this.props.fallback) {
        return this.props.fallback
      }

      // 或使用 fallbackRender
      if (this.props.fallbackRender) {
        return this.props.fallbackRender({
          error: this.state.error,
          resetErrorBoundary: this.handleReset,
        })
      }

      // 預設的 fallback UI
      return (
        <div className="error-container">
          <div className="error-icon">⚠️</div>
          <h2>糟糕,發生了一些問題</h2>
          <p className="error-message">{this.state.error?.message || '未知錯誤'}</p>
          <button className="retry-button" onClick={this.handleReset}>
            重試
          </button>
          {process.env.NODE_ENV === 'development' && (
            <details className="error-details">
              <summary>錯誤詳情</summary>
              <pre>{this.state.errorInfo?.componentStack}</pre>
            </details>
          )}
        </div>
      )
    }

    return this.props.children
  }
}

// 使用
function App() {
  return (
    <ErrorBoundary
      onError={(error, info) => {
        // 發送到錯誤追蹤服務
        console.error('Error caught:', error)
      }}
      onReset={() => {
        // 重置應用程式狀態
        window.location.reload()
      }}
    >
      <MainApp />
    </ErrorBoundary>
  )
}