如何处理 React 中的错误

作为开发者如何优雅的处理错误是至关重要的,否则页面出现白屏影响用户体验甚至流失用户。

下面通过不同的方式来处理 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 捕获错误。

image.png

部分 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

错误边界并不是万能的。不会捕获以下错误:

  1. 事件处理器
  2. 异步(例如 setTimeout 或 requestAnimationFrame 回调)
  3. 服务端渲染
  4. 在错误边界本身(而不是其子项)中引发的错误

仍然需要 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,事件处理程序,服务器端渲染以及错误边界本身引发的错误)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,639评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,277评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,221评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,474评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,570评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,816评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,957评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,718评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,176评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,511评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,646评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,322评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,934评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,755评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,987评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,358评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,514评论 2 348

推荐阅读更多精彩内容