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,Header 和 Footer 仍然正常顯示。
多個錯誤邊界
你可以使用多個錯誤邊界來隔離不同區域的錯誤:
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>
}
錯誤邊界的限制
錯誤邊界無法捕捉以下類型的錯誤:
事件處理函式中的錯誤
function Button() { function handleClick() { throw new Error('錯誤') // 錯誤邊界無法捕捉 } return <button onClick={handleClick}>Click</button> }解決方案:在事件處理函式中使用 try-catch
function handleClick() { try { riskyOperation() } catch (error) { // 處理錯誤 setError(error) } }非同步程式碼
useEffect(() => { fetch('/api').catch((error) => { // 錯誤邊界無法捕捉 }) }, [])伺服器端渲染 (SSR)
錯誤邊界本身的錯誤
搭配 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>
)
}