使用 react-error-boundary 来处理 React 中的错误

1,585 阅读6分钟

这段代码有什么问题?

import * as React from 'react'
import ReactDOM from 'react-dom'

function Greeting({subject}) {
  return <div>Hello {subject.toUpperCase()}</div>
}

function Farewell({subject}) {
  return <div>Goodbye {subject.toUpperCase()}</div>
}

function App() {
  return (
    <div>
      <Greeting />
      <Farewell />
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))

如果你把它发送到生产中,你的用户会得到悲哀的白屏。

Chrome window with nothing but white

如果你用create-react-app的错误叠加功能运行这个程序(在开发过程中),你会得到这个结果。

TypeError Cannot read property 'toUpperCase' of undefined

问题是我们需要传递一个subject 的道具(作为一个字符串)或者默认subject 的道具值。很明显,这是设计好的,但是运行时错误经常发生,这就是为什么优雅地处理这种错误是个好主意。因此,让我们暂时不考虑这个错误,看看React有什么工具可以让我们处理这样的运行时错误。

try/catch?

处理这种错误的天真方法是添加一个try/catch

import * as React from 'react'
import ReactDOM from 'react-dom'

function ErrorFallback({error}) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre style={{color: 'red'}}>{error.message}</pre>
    </div>
  )
}

function Greeting({subject}) {
  try {
    return <div>Hello {subject.toUpperCase()}</div>
  } catch (error) {
    return <ErrorFallback error={error} />
  }
}

function Farewell({subject}) {
  try {
    return <div>Goodbye {subject.toUpperCase()}</div>
  } catch (error) {
    return <ErrorFallback error={error} />
  }
}

function App() {
  return (
    <div>
      <Greeting />
      <Farewell />
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))

这就 "管用 "了。

但是,这可能是我的可笑之处,但如果我不想把我的应用程序中的每个组件都包裹在一个try/catch 块中呢?在常规的JavaScript中,你可以简单地将调用的函数包裹在一个try/catch ,它将捕捉到它所调用的函数中的任何错误。让我们在这里尝试一下。

import * as React from 'react'
import ReactDOM from 'react-dom'

function ErrorFallback({error}) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre style={{color: 'red'}}>{error.message}</pre>
    </div>
  )
}

function Greeting({subject}) {
  return <div>Hello {subject.toUpperCase()}</div>
}

function Farewell({subject}) {
  return <div>Goodbye {subject.toUpperCase()}</div>
}

function App() {
  try {
    return (
      <div>
        <Greeting />
        <Farewell />
      </div>
    )
  } catch (error) {
    return <ErrorFallback error={error} />
  }
}

ReactDOM.render(<App />, document.getElementById('root'))

不幸的是,这并不奏效。这是因为我们不是调用GreetingFarewell 的人。React调用这些函数。当我们在JSX中使用它们时,我们只是用这些函数创建React元素作为type 。告诉React,"如果App ,这里是需要调用的其他组件"。但我们实际上并没有调用它们,所以try/catch不会工作。

说实话,我对此并不太失望,因为try/catch 本身就是命令式的,反正我更喜欢用声明式的方式来处理我的应用程序中的错误。

React错误边界

这就是错误边界功能发挥作用的地方。一个 "错误边界 "是一个特殊的组件,你写它来处理上述的运行时错误。一个组件要成为 "错误边界"。

  1. 它必须是一个类组件 🙁
  2. 它必须实现getDerivedStateFromErrorcomponentDidCatch

幸运的是,我们有 react-error-boundary 它暴露了任何人都需要编写的最后一个错误边界组件,因为它为你提供了在React应用程序中声明性地处理运行时错误所需的所有工具。

因此,让我们添加 react-error-boundary并渲染ErrorBoundary 组件。

import * as React from 'react'
import ReactDOM from 'react-dom'
import {ErrorBoundary} from 'react-error-boundary'

function ErrorFallback({error}) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre style={{color: 'red'}}>{error.message}</pre>
    </div>
  )
}

function Greeting({subject}) {
  return <div>Hello {subject.toUpperCase()}</div>
}

function Farewell({subject}) {
  return <div>Goodbye {subject.toUpperCase()}</div>
}

function App() {
  return (
    <div>
      <ErrorBoundary FallbackComponent={ErrorFallback}>
        <Greeting />
        <Farewell />
      </ErrorBoundary>
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))

这样就可以完美地工作了。

错误恢复

这样做的好处是,你几乎可以把ErrorBoundary组件和你做try/catch 块的方式一样。你可以把它包在一堆React组件中来处理大量的错误,或者你可以把它的范围缩小到树的一个特定部分,以获得更细化的错误处理和恢复。react-error-boundary脚本中提供了我们需要的所有工具来管理这个问题。

这里有一个更复杂的例子。

function ErrorFallback({error, resetErrorBoundary}) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre style={{color: 'red'}}>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  )
}

function Bomb({username}) {
  if (username === 'bomb') {
    throw new Error('💥 CABOOM 💥')
  }
  return `Hi ${username}`
}

function App() {
  const [username, setUsername] = React.useState('')
  const usernameRef = React.useRef(null)

  return (
    <div>
      <label>
        {`Username (don't type "bomb"): `}
        <input
          placeholder={`type "bomb"`}
          value={username}
          onChange={e => setUsername(e.target.value)}
          ref={usernameRef}
        />
      </label>
      <div>
        <ErrorBoundary
          FallbackComponent={ErrorFallback}
          onReset={() => {
            setUsername('')
            usernameRef.current.focus()
          }}
          resetKeys={[username]}
        >
          <Bomb username={username} />
        </ErrorBoundary>
      </div>
    </div>
  )
}

下面是这种体验。

用户名(不要输入 "炸弹")。

你好

你会注意到,如果你输入了 "bomb",Bomb 组件就会被ErrorFallback 组件取代,你可以通过改变username(因为那是在resetKeys 道具中)或者点击 "再试一次 "来恢复,因为那是与resetErrorBoundary 连接的,我们有一个onReset ,可以将我们的状态重置为一个不会重新触发错误的用户名。

处理所有的错误

不幸的是,有一些错误React并没有/不能交给我们的错误边界。引用React文档

错误边界不捕捉以下错误。

  • 事件处理程序(了解更多
  • 异步代码(例如 setTimeout 或 requestAnimationFrame 回调)。
  • 服务器端的渲染
  • 在错误边界本身(而不是其子女)中抛出的错误

大多数时候,人们会管理一些error 状态,并在发生错误时渲染一些不同的东西,就像这样。

function Greeting() {
  const [{status, greeting, error}, setState] = React.useState({
    status: 'idle',
    greeting: null,
    error: null,
  })

  function handleSubmit(event) {
    event.preventDefault()
    const name = event.target.elements.name.value
    setState({status: 'pending'})
    fetchGreeting(name).then(
      newGreeting => setState({greeting: newGreeting, status: 'resolved'}),
      newError => setState({error: newError, status: 'rejected'}),
    )
  }

  return status === 'rejected' ? (
    <ErrorFallback error={error} />
  ) : status === 'resolved' ? (
    <div>{greeting}</div>
  ) : (
    <form onSubmit={handleSubmit}>
      <label>Name</label>
      <input id="name" />
      <button type="submit" onClick={handleClick}>
        get a greeting
      </button>
    </form>
  )
}

不幸的是,这样做意味着你必须维护两种方法来处理错误。

  1. 运行时错误
  2. fetchGreeting 错误

幸运的是。 react-error-boundary也暴露了一个简单的钩子来帮助处理这些情况。下面是你如何使用它来完全回避这个问题。

function Greeting() {
  const [{status, greeting}, setState] = React.useState({
    status: 'idle',
    greeting: null,
  })
  const handleError = useErrorHandler()

  function handleSubmit(event) {
    event.preventDefault()
    const name = event.target.elements.name.value
    setState({status: 'pending'})
    fetchGreeting(name).then(
      newGreeting => setState({greeting: newGreeting, status: 'resolved'}),
      error => handleError(error),
    )
  }

  return status === 'resolved' ? (
    <div>{greeting}</div>
  ) : (
    <form onSubmit={handleSubmit}>
      <label>Name</label>
      <input id="name" />
      <button type="submit" onClick={handleClick}>
        get a greeting
      </button>
    </form>
  )
}

因此,当我们的fetchGreeting 承诺被拒绝时,handleError 函数会被调用,并且 react-error-boundary 会像往常一样使其传播到最近的错误边界。

另外,假设你使用的是一个钩子,它给了你错误。

function Greeting() {
  const [name, setName] = React.useState('')
  const {status, greeting, error} = useGreeting(name)
  useErrorHandler(error)

  function handleSubmit(event) {
    event.preventDefault()
    const name = event.target.elements.name.value
    setName(name)
  }

  return status === 'resolved' ? (
    <div>{greeting}</div>
  ) : (
    <form onSubmit={handleSubmit}>
      <label>Name</label>
      <input id="name" />
      <button type="submit" onClick={handleClick}>
        get a greeting
      </button>
    </form>
  )
}

在这种情况下,如果error 曾经被设置为一个真实的值,那么它将被传播到最近的错误边界。

在任何一种情况下,你都可以这样处理这些错误。

const ui = (
  <ErrorBoundary FallbackComponent={ErrorFallback}>
    <Greeting />
  </ErrorBoundary>
)

而现在这将处理你的运行时错误以及fetchGreetinguseGreeting 代码中的异步错误。

注意:你可能有兴趣知道,useErrorHandler实现只有6行长 😉

结论

Error Boundaries在React中已经有很多年了,但我们仍然处于这种尴尬的境地,既要用Error Boundaries处理运行时的错误,又要在我们的组件中处理其他的错误状态,而我们最好在这两方面都重复使用我们的Error Boundary组件。如果你还没有给react-error-boundary试试,一定要给它一个可靠的外观

祝您好运。

哦,还有一件事。现在,你可能会注意到,即使错误被你的错误边界处理了,你也会遇到那个错误叠加。这只会在开发过程中发生(如果你使用的是支持该功能的开发服务器,如 react-scripts、gatsby 或 codesandbox)。它不会在生产中出现。是的,我同意这很烦人,欢迎提交PR