作为开发者如何优雅的处理错误是至关重要的,否则页面出现白屏影响用户体验甚至流失用户。
下面通过不同的方式来处理 React 中的错误。
try...catch
js 中捕获错误用的最多的方式就是 try...catch
try {
// do something...
} catch (e) {
console.error(e)
}
在 React 中的某些场景下也会用到 try...catch
,比如网络请求:
const fetchData = async () => {
try {
return await fetch('http://xxx.com')
} catch (error) {
console.error(error)
}
}
遗憾的是 try...catch
仅适用于命令式代码,不适用于组件中编写的 JSX 之类的声明式代码。所以这就是为什么不会把整个应用程序包裹在 try...catch
中。
React 错误边界
了解错误边界之前先考虑下为什么需要错误边界,假如有一个这样的组件:
const AppComponent = props => {
return <span>{props.userinfo.name}</span>
}
export default AppComponent
function App() {
return <AppComponent />
}
当访问不存的 props.userinfo 时,出现如下错误而导致整个页面空白,用户将无法操作或查看任何内容。但这种情况无法用 try...catch 捕获错误。
部分 js 错误不应该导致整个程序崩溃,为了解决这个问题,React 16 引入了一个新的概念 —— 错误边界。
错误边界是一种 class 组件,这种组件可以捕获发生在其子组件树任何位置的 js 错误,并打印这些错误,同时展示降级 UI,而并不会渲染那些发生错误的子组件树。错误边界可以捕获发生在整个子组件树的渲染期间、生命周期方法以及构造函数中的错误。该组件定义了一个(或两个)生命周期方法 static getDerivedStateFromError()
或 componentDidCatch()
时,就变成了错误边界。当抛出错误后使用static getDerivedStateFromError()
渲染备用 UI。 使用componentDidCatch()
打印错误信息或者将错误上报远程服务。
import React, { Component } from 'react'
class ErrorComp extends Component {
constructor(props) {
super(props)
this.state = {
hasError: false
}
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return {
hasError: true,
error
}
}
componentDidCatch(error, errorInfo) {
// 收集错误
console.log(error)
console.log(errorInfo)
}
render() {
const { hasError, error } = this.state
if (hasError) {
return (
<div>
<p>出错了~ 😭</p>
{error.message && <span>错误信息: {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
错误边界并不是万能的。不会捕获以下错误:
- 事件处理器
- 异步(例如 setTimeout 或 requestAnimationFrame 回调)
- 服务端渲染
- 在错误边界本身(而不是其子项)中引发的错误
仍然需要 try...catch 处理。
事件处理程序中的错误捕获
错误边界无法捕获事件处理器内部的错误。而且也不需要错误边界来捕获事件处理器中的错误,因为事件处理器与 render、生命周期不同,触发的时机不是在渲染期间。因此如果事件处理中出现错误,仍然需要追踪上报那么就需要 try...catch:
class MyComponent extends Component {
state = {
error: null
}
handleClick = () => {
try {
// 执行操作,如有错误则会抛出
throw Error('oops...')
} catch (error) {
this.setState({ error })
}
}
render() {
console.log(this.state)
if (this.state.error) {
return <h1>出错了😭 {this.state.error.message}</h1>
}
return <button onClick={this.handleClick}>Click Me</button>
}
}
setTimeout 调用中的错误捕获
改造 handleClick:
handleClick = () => {
setTimeout(() => {
try {
// 执行操作,如有错误则会抛出
throw Error('oops...')
} catch (error) {
this.setState({ error })
}
}, 1000)
}
react-error-boundary
社区提供了一种错误边界的三方组件。
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>出错了~ 😭</p>
{error.message && <span>错误信息: {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>
)
}
该模块还有一个好处是提供了 withErrorBoundary
方法,可以将函数组件包装成高阶组件(HOC)
import React, { withErrorBoundary } from 'react-error-boundary'
const AppComponent = props => <span>{props.userinfo.name}</span>
const ErrorFallback = ({ error, resetErrorBoundary }) => (
<div>
<p>出错了~ 😭</p>
{error.message && <span>错误信息: {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
自己实现 React 错误边界
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 }
总结
React 错误边界非常适合在声明式代码中捕获错误。对于其他情况,需要使用 try...catch(例如,异步调用 setTimeout,事件处理程序,服务器端渲染以及错误边界本身引发的错误)。