这段代码有什么问题?
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'))
如果你把它发送到生产中,你的用户会得到悲哀的白屏。
如果你用create-react-app的错误叠加功能运行这个程序(在开发过程中),你会得到这个结果。
问题是我们需要传递一个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'))
不幸的是,这并不奏效。这是因为我们不是调用Greeting
和Farewell
的人。React调用这些函数。当我们在JSX中使用它们时,我们只是用这些函数创建React元素作为type
。告诉React,"如果App
,这里是需要调用的其他组件"。但我们实际上并没有调用它们,所以try/catch
不会工作。
说实话,我对此并不太失望,因为try/catch
本身就是命令式的,反正我更喜欢用声明式的方式来处理我的应用程序中的错误。
React错误边界
这就是错误边界功能发挥作用的地方。一个 "错误边界 "是一个特殊的组件,你写它来处理上述的运行时错误。一个组件要成为 "错误边界"。
- 它必须是一个类组件 🙁
- 它必须实现
getDerivedStateFromError
或componentDidCatch
。
幸运的是,我们有 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>
)
}
不幸的是,这样做意味着你必须维护两种方法来处理错误。
- 运行时错误
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>
)
而现在这将处理你的运行时错误以及fetchGreeting
或useGreeting
代码中的异步错误。
注意:你可能有兴趣知道,useErrorHandler
的实现只有6行长 😉
结论
Error Boundaries在React中已经有很多年了,但我们仍然处于这种尴尬的境地,既要用Error Boundaries处理运行时的错误,又要在我们的组件中处理其他的错误状态,而我们最好在这两方面都重复使用我们的Error Boundary组件。如果你还没有给react-error-boundary
试试,一定要给它一个可靠的外观
祝您好运。
哦,还有一件事。现在,你可能会注意到,即使错误被你的错误边界处理了,你也会遇到那个错误叠加。这只会在开发过程中发生(如果你使用的是支持该功能的开发服务器,如 react-scripts、gatsby 或 codesandbox)。它不会在生产中出现。是的,我同意这很烦人,欢迎提交PR。