如何處理 React 中的錯(cuò)誤

作為開發(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ò)誤牺弄。

image.png

部分 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ò)誤:

  1. 事件處理器
  2. 異步(例如 setTimeout 或 requestAnimationFrame 回調(diào))
  3. 服務(wù)端渲染
  4. 在錯(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ò)誤)毫玖。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末掀虎,一起剝皮案震驚了整個(gè)濱河市凌盯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌烹玉,老刑警劉巖驰怎,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異二打,居然都是意外死亡县忌,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門继效,熙熙樓的掌柜王于貴愁眉苦臉地迎上來症杏,“玉大人,你說我怎么就攤上這事瑞信≡Т龋” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵喧伞,是天一觀的道長走芋。 經(jīng)常有香客問我,道長潘鲫,這世上最難降的妖魔是什么翁逞? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮溉仑,結(jié)果婚禮上挖函,老公的妹妹穿的比我還像新娘。我一直安慰自己浊竟,他們只是感情好怨喘,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著振定,像睡著了一般必怜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上后频,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天梳庆,我揣著相機(jī)與錄音,去河邊找鬼卑惜。 笑死膏执,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的露久。 我是一名探鬼主播更米,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼毫痕!你這毒婦竟也來了征峦?” 一聲冷哼從身側(cè)響起纸巷,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎眶痰,沒想到半個(gè)月后瘤旨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡竖伯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年存哲,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片七婴。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡祟偷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出打厘,到底是詐尸還是另有隱情修肠,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布户盯,位于F島的核電站嵌施,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏莽鸭。R本人自食惡果不足惜吗伤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望硫眨。 院中可真熱鬧足淆,春花似錦、人聲如沸礁阁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽姥闭。三九已至丹鸿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間泣栈,已是汗流浹背卜高。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留南片,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓庭敦,卻偏偏與公主長得像疼进,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子秧廉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容