作為開發(fā)者如何優(yōu)雅的處理錯(cuò)誤是至關(guān)重要的碎紊,否則頁面出現(xiàn)白屏影響用戶體驗(yàn)甚至流失用戶甫恩。
下面通過不同的方式來處理 React 中的錯(cuò)誤鸣个。
try...catch
js 中捕獲錯(cuò)誤用的最多的方式就是 try...catch
try {
// do something...
} catch (e) {
console.error(e)
}
在 React 中的某些場景下也會(huì)用到 try...catch
帅矗,比如網(wǎng)絡(luò)請求:
const fetchData = async () => {
try {
return await fetch('http://xxx.com')
} catch (error) {
console.error(error)
}
}
遺憾的是 try...catch
僅適用于命令式代碼匣砖,不適用于組件中編寫的 JSX 之類的聲明式代碼科吭。所以這就是為什么不會(huì)把整個(gè)應(yīng)用程序包裹在 try...catch
中昏滴。
React 錯(cuò)誤邊界
了解錯(cuò)誤邊界之前先考慮下為什么需要錯(cuò)誤邊界,假如有一個(gè)這樣的組件:
const AppComponent = props => {
return <span>{props.userinfo.name}</span>
}
export default AppComponent
function App() {
return <AppComponent />
}
當(dāng)訪問不存的 props.userinfo 時(shí)对人,出現(xiàn)如下錯(cuò)誤而導(dǎo)致整個(gè)頁面空白谣殊,用戶將無法操作或查看任何內(nèi)容。但這種情況無法用 try...catch 捕獲錯(cuò)誤牺弄。
部分 js 錯(cuò)誤不應(yīng)該導(dǎo)致整個(gè)程序崩潰姻几,為了解決這個(gè)問題,React 16 引入了一個(gè)新的概念 —— 錯(cuò)誤邊界势告。
錯(cuò)誤邊界是一種 class 組件蛇捌,這種組件可以捕獲發(fā)生在其子組件樹任何位置的 js 錯(cuò)誤,并打印這些錯(cuò)誤咱台,同時(shí)展示降級(jí) UI络拌,而并不會(huì)渲染那些發(fā)生錯(cuò)誤的子組件樹。錯(cuò)誤邊界可以捕獲發(fā)生在整個(gè)子組件樹的渲染期間回溺、生命周期方法以及構(gòu)造函數(shù)中的錯(cuò)誤春贸。該組件定義了一個(gè)(或兩個(gè))生命周期方法 static getDerivedStateFromError()
或 componentDidCatch()
時(shí),就變成了錯(cuò)誤邊界遗遵。當(dāng)拋出錯(cuò)誤后使用static getDerivedStateFromError()
渲染備用 UI萍恕。 使用componentDidCatch()
打印錯(cuò)誤信息或者將錯(cuò)誤上報(bào)遠(yuǎn)程服務(wù)。
import React, { Component } from 'react'
class ErrorComp extends Component {
constructor(props) {
super(props)
this.state = {
hasError: false
}
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能夠顯示降級(jí)后的 UI
return {
hasError: true,
error
}
}
componentDidCatch(error, errorInfo) {
// 收集錯(cuò)誤
console.log(error)
console.log(errorInfo)
}
render() {
const { hasError, error } = this.state
if (hasError) {
return (
<div>
<p>出錯(cuò)了~ ??</p>
{error.message && <span>錯(cuò)誤信息: {error.message}</span>}
</div>
)
}
return this.props.children
}
}
const AppComponent = props => {
return <span>{props.userinfo.name}</span>
}
function App() {
return (
<ErrorComp>
<AppComponent />
</ErrorComp>
)
}
export default App
錯(cuò)誤邊界并不是萬能的车要。不會(huì)捕獲以下錯(cuò)誤:
- 事件處理器
- 異步(例如 setTimeout 或 requestAnimationFrame 回調(diào))
- 服務(wù)端渲染
- 在錯(cuò)誤邊界本身(而不是其子項(xiàng))中引發(fā)的錯(cuò)誤
仍然需要 try...catch 處理允粤。
事件處理程序中的錯(cuò)誤捕獲
錯(cuò)誤邊界無法捕獲事件處理器內(nèi)部的錯(cuò)誤。而且也不需要錯(cuò)誤邊界來捕獲事件處理器中的錯(cuò)誤屯蹦,因?yàn)槭录幚砥髋c render维哈、生命周期不同绳姨,觸發(fā)的時(shí)機(jī)不是在渲染期間登澜。因此如果事件處理中出現(xiàn)錯(cuò)誤,仍然需要追蹤上報(bào)那么就需要 try...catch:
class MyComponent extends Component {
state = {
error: null
}
handleClick = () => {
try {
// 執(zhí)行操作飘庄,如有錯(cuò)誤則會(huì)拋出
throw Error('oops...')
} catch (error) {
this.setState({ error })
}
}
render() {
console.log(this.state)
if (this.state.error) {
return <h1>出錯(cuò)了?? {this.state.error.message}</h1>
}
return <button onClick={this.handleClick}>Click Me</button>
}
}
setTimeout 調(diào)用中的錯(cuò)誤捕獲
改造 handleClick:
handleClick = () => {
setTimeout(() => {
try {
// 執(zhí)行操作脑蠕,如有錯(cuò)誤則會(huì)拋出
throw Error('oops...')
} catch (error) {
this.setState({ error })
}
}, 1000)
}
react-error-boundary
社區(qū)提供了一種錯(cuò)誤邊界的三方組件。
pnpm add react-error-boundary
import React, { ErrorBoundary } from 'react-error-boundary'
const AppComponent = props => {
return <span>{props.userinfo.name}</span>
}
const ErrorFallback = ({ error, resetErrorBoundary }) => (
<div>
<p>出錯(cuò)了~ ??</p>
{error.message && <span>錯(cuò)誤信息: {error.message}</span>}
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, { componentStack }) => {
console.error(error)
console.log(componentStack)
}}
onReset={() => {
console.log('reset')
}}
>
<AppComponent />
</ErrorBoundary>
)
}
該模塊還有一個(gè)好處是提供了 withErrorBoundary
方法跪削,可以將函數(shù)組件包裝成高階組件(HOC)
import React, { withErrorBoundary } from 'react-error-boundary'
const AppComponent = props => <span>{props.userinfo.name}</span>
const ErrorFallback = ({ error, resetErrorBoundary }) => (
<div>
<p>出錯(cuò)了~ ??</p>
{error.message && <span>錯(cuò)誤信息: {error.message}</span>}
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)
const App = withErrorBoundary(AppComponent, {
FallbackComponent: ErrorFallback,
onError: (error, { componentStack }) => {
console.error(error)
console.log(componentStack)
}
})
export default App
自己實(shí)現(xiàn) React 錯(cuò)誤邊界
import React, { Component } from 'react'
const initialState = { error: null }
export default class ErrorBoundary extends Component {
static getDerivedStateFromError(error) {
return {
error
}
}
state = initialState
resetErrorBoundary = (...args) => {
this.props.onReset?.(...args)
this.reset()
}
reset() {
this.setState(initialState)
}
componentDidCatch(error, errorInfo) {
this.props.onError?.(error, errorInfo)
}
render() {
const { error } = this.state
const { FallbackComponent } = this.props
const props = {
error,
resetErrorBoundary: this.resetErrorBoundary
}
if (error !== null) {
return <FallbackComponent {...props} />
}
return this.props.children
}
}
function withErrorBoundary(ChildComp, errorBoundaryProps) {
return props => (
<ErrorBoundary {...errorBoundaryProps}>
<ChildComp {...props} />
</ErrorBoundary>
)
}
export { ErrorBoundary, withErrorBoundary }
總結(jié)
React 錯(cuò)誤邊界非常適合在聲明式代碼中捕獲錯(cuò)誤谴仙。對于其他情況,需要使用 try...catch(例如碾盐,異步調(diào)用 setTimeout晃跺,事件處理程序,服務(wù)器端渲染以及錯(cuò)誤邊界本身引發(fā)的錯(cuò)誤)毫玖。