2023-02-如何处理react的异常完整指南
我们都希望我们的应用是稳健的,工作高效的,而且可预知每一个异常情况。不是吗?但是,令人沮丧的是,我们都只是普通人,我们总是写bug,而且不存在没有bug的代码。无论我们怎么努力或者做了多少次测试,总是有一些可怕bug出现。最重要的,对于用户体验来说,能优雅的处理bug产生的错误是一件很有挑战性的工作。尽可能定位bug的位置,尽最大努力处理它,直到它被彻底修复。 所以今天,我们来看一下react中怎么处理错误的情况。当一个错误出现时,我们应该怎么办;不同的错误捕捉方式有什么不同,怎么正确的选择。
1. 为什么我们要在React中捕获错误
首先第一件事,React中有一些错误捕获方式为什么很重要? 简单的回答是:从react16开始,当在react的生命周期中报错时,react会卸载掉整个app。在此之前,发生错误时所有的组件是在页面上展示的。但是现在,一个位置的ui上的错误,或者你依赖的一个第三方库的错误,都会使你的页面展示一张空白,像被销毁了一样。 在此之前,前端开发从来没有这么大的破坏力。
2. 还记的我们在js中怎么捕获异常吗
当我们在普通js中捕获那些令人讨厌的惊喜时,方式是非常简单的 当我们使用我们旧的方式try-catch
时,它逻辑上是很清晰的,try
里包裹可能出错的代码,catch
中捕获那些错误。
try{
doSomething();
}catch(e){
}
async函数中也同样适用
try{
await fetch()
}catch(e){
}
promise函数也有catch方法,像下面这样
fetch()
.then()
.catch(()=>{
})
这都是相同的概念,只是实现上有不同,所以接下来的文章,我们要用try-catch
处理所有的错误。
3. React简单使用try-catch的注意事项
当我们捕获一个异常时,我们需要处理它,对吗?所以,正确的处理方式是什么?难道不是找个地方打印它吗?或者更确切的,难道我们就把这个空白大页面这样不友好的扔在那里吗? 最直观的解决方法就是在我们修复之前,展示点别的什么东西。幸运的是,我们可以在catch语句块中做任何事情,我们可以设置像下面一样的state.
const SomeComponent = () => {
const [hasError, setHasError] = useState(false);
useEffect(() => {
try {
// do something like fetching some data
} catch(e) {
// oh no! the fetch failed, we have no data to render!
setHasError(true);
}
})
// something happened during fetch, lets render some nice error screen
if (hasError) return <SomeErrorScreen />
// all's good, data is here, let's render it
return <SomeComponentContent {...datasomething} />
}
我们尝试发送一个fetch请求,如果它失败了,设置错误状态,如果error的状态是true,我们就render错误的页面给用户,再加一些其他信息,比如重试。 这种方式是非常简单高效的,这个例子也仅仅是fetch的例子。 如果你想捕获一个组件中所有可能发生的错误,你将面临一些挑战和局限。
3.1 局限1: 你将要处理useEffect的错误
如果我们用try-catch包括useEffect,它将不会捕获错误,像下面这样
try {
useEffect(() => {
throw new Error('Hulk smash!');
}, [])
} catch(e) {
// useEffect throws, but this will never be called
}
因为useEffect是异步执行的,try-catch里的代码执行时,useEffect并没有执行,其他代码不会报错,所以catch没有捕获错误。 我们可以改成这样
useEffect(() => {
try {
throw new Error('Hulk smash!');
} catch(e) {
// this one will be caught
}
}, [])
这个是可行的,不仅仅是useEffect,其他任何异步的方式这样都是可以捕获异常的。但是,你必须把你的代码分成两块,而且每个hook都要这么写。
3.2 局限2: 子组件
try-catch不支持子组件的写法。假如child组件中报错,我们需要捕获,我们似乎可以写成这样:
const Component = () => {
let child;
try {
child = <Child />
} catch(e) {
// useless for catching errors inside Child component, won't be triggered
}
return child;
}
const Component = () => {
try {
return <Child />
} catch(e) {
// still useless for catching errors inside Child component, won't be triggered
}
}
上面两种都不会捕获到错误,因为我们写<Child />
时,我们并不是真正的渲染它,我们只是创建了一个Element对象,它只是一个组件的定义,只是一个包含了type和props的组件定义。它之后会被react真正的执行渲染。所以这里的try-catch不会报错。
3.3 局限3: 渲染时改变状态是不可取的
如果我们想捕获useEffect外面的错误,像下面这样
const Component = () => {
const [hasError, setHasError] = useState(false);
try {
doSomethingComplicated();
} catch(e) {
// don't do that! will cause infinite loop in case of an error
setHasError(true);
}
}
这样的代码会导致死循环,我们其实只需要在catch里返回一个降级页面
const Component = () => {
try {
doSomethingComplicated();
} catch(e) {
// this allowed
return <SomeErrorScreen />
}
}
但是,跟你想的一样,这只是冰山一角,我们需要在一个组件中用不同的方式处理错误。像下面这样
// while it will work, it's super cumbersome and hard to maitain, don't do that
const SomeComponent = () => {
const [hasError, setHasError] = useState(false);
useEffect(() => {
try {
// do something like fetching some data
} catch(e) {
// can't just return in case of errors in useEffect or callbacks
// so have to use state
setHasError(true);
}
})
try {
// do something during render
} catch(e) {
// but here we can't use state, so have to return directly in case of an error
return <SomeErrorScreen />;
}
// and still have to return in case of error state here
if (hasError) return <SomeErrorScreen />
return <SomeComponentContent {...datasomething} />
}
我们总结一下,如果我们在react中使用try-catch,我们将在捕获了错误的同时,写下了大量的冗余代码。 幸运的是,我们有其他方式
4. React ErrorBoundary
为了解决上面的麻烦,react给我们提供了ErrorBoundary,一个特殊的API在react代码里,在某种程度上替换了try-catch。
const Component = () => {
return (
<ErrorBoundary>
<SomeChildComponent />
<AnotherChildComponent />
</ErrorBoundary>
)
}
现在,只要在他们组件或者他们子组件的render方法里报错,我们都可以捕获并处理他们。 但是react本身并没有提供组件,它只是告诉我们怎么实现他,简单的实现大概如下
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
// initialize the error state
this.state = { hasError: false };
}
// if an error happened, set the state to true
static getDerivedStateFromError(error) {
return { hasError: true };
}
render() {
// if error happened, return a fallback component
if (this.state.hasError) {
return <>Oh no! Epic fail!</>
}
return this.props.children;
}
}
我们创建了一个常规的class组件,实现了getDerivedStateFromError方法,这个方法把这个组件变成了ErrorBoundary
。 另一件重要的事情是,我们要处理这个错误,把这个错误发给那个解决它的人。所以,react还提供了componentDidCatch
方法。
class ErrorBoundary extends React.Component {
// everything else stays the same
componentDidCatch(error, errorInfo) {
// send error to somewhere here
log(error, errorInfo);
}
}
在设置好这些之后,我们可以再优化一下ErrorBoundary,把fallback当做props传入
render() {
// if error happened, return a fallback component
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
使用的时候
const Component = () => {
return (
<ErrorBoundary fallback={<>Oh no! Do something!</>}>
<SomeChildComponent />
<AnotherChildComponent />
</ErrorBoundary>
)
}
但是当我们在一个按钮点击事件上发生错误时,它捕获不到。
const add = () => {
throw new Error('事件error');
}
这就告诉我们,ErrorBoundary不是万能的。
5. ErrorBoundary的局限
ErrorBoundary只能捕获React生命周期中的错误,一些生命周期之外的比如promise、异步代码、回调函数和事件处理,如果我们不处理,会怎么样呢?
const Component = () => {
useEffect(() => {
// this one will be caught by ErrorBoundary component
throw new Error('Destroy everything!');
}, [])
const onClick = () => {
// this error will just disappear into the void
throw new Error('Hulk smash!');
}
useEffect(() => {
// if this one fails, the error will also disappear
fetch('/bla')
}, [])
return <button onClick={onClick}>click me</button>
}
const ComponentWithBoundary = () => {
return (
<ErrorBoundary>
<Component />
</ErrorBoundary>
)
}
常规的推荐做法是用try-catch捕获这些错误。如下
const Component = () => {
const [hasError, setHasError] = useState(false);
// most of the errors in this component and in children will be caught by the ErrorBoundary
const onClick = () => {
try {
// this error will be caught by catch
throw new Error('Hulk smash!');
} catch(e) {
setHasError(true);
}
}
if (hasError) return 'something went wrong';
return <button onClick={onClick}>click me</button>
}
const ComponentWithBoundary = () => {
return (
<ErrorBoundary fallback={"Oh no! Something went wrong"}>
<Component />
</ErrorBoundary>
)
}
这样我们就又回到了原点: 在一个组件中用不同的方式处理不同的错误。 我们也可以这样,不在组件级别上做处理,而是用props,如下
const Component = ({ onError }) => {
const onClick = () => {
try {
throw new Error('Hulk smash!');
} catch(e) {
// just call a prop instead of maintaining state here
onError();
}
}
return <button onClick={onClick}>click me</button>
}
const ComponentWithBoundary = () => {
const [hasError, setHasError] = useState();
const fallback = "Oh no! Something went wrong";
if (hasError) return fallback;
return (
<ErrorBoundary fallback={fallback}>
<Component onError={() => setHasError(true)} />
</ErrorBoundary>
)
}
但是这又增加了额外的代码,我们不得不在每个子组件上都这样做。何况我们要在两个地方维护错误状态,组件里和ErrorBoundary。由于ErrorBoundary已经做了全部的工作,我们不希望做两份。 我们可以用ErrorBoundary做异步错误的捕获吗?
6. 用ErrorBoundary做异步错误捕获
有趣的是,我们可以用ErrorBoundary捕获这些,大众偶像Dan Abramov分享给我们一个hack方法
Throwing Error from hook not caught in error boundary · Issue #14981 · facebook/react.
这个方法首先还是用try-catch捕获异常,然后在catch中触发react的渲染,在渲染中抛出异常。这样ErrorBoundary就可以在任何地方捕获错误了。 而且由于state更新是触发重新渲染的原因,setState函数可以接受一个函数作为参数,我们可以写成这样
const Component = () => {
// create some random state that we'll use to throw errors
const [state, setState] = useState();
const onClick = () => {
try {
// something bad happened
} catch (e) {
// trigger state update, with updater function as an argument
setState(() => {
// re-throw this error within the updater function
// it will be triggered during state update
throw e;
})
}
}
}
最后一步我们需要提取这种hack方式,我们不必在每个组件中都创建一个state,我们可以用hook自定义
const useThrowAsyncError = () => {
const [state, setState] = useState();
return (error) => {
setState(() => throw error)
}
}
这样使用
const Component = () => {
const throwAsyncError = useThrowAsyncError();
useEffect(() => {
fetch('/bla').then().catch((e) => {
// throw async error here!
throwAsyncError(e)
})
})
}
我们也可以封装接受函数的hook
const useCallbackWithErrorHandling = (callback) => {
const [state, setState] = useState();
return (...args) => {
try {
callback(...args);
} catch(e) {
setState(() => throw e);
}
}
}
使用如下
const Component = () => {
const onClick = () => {
// do something dangerous here
}
const onClickWithErrorHandler = useCallbackWithErrorHandling(onClick);
return <button onClick={onClickWithErrorHandler}>click me!</button>
}
这样,既满足你的期望,又达到了程序的要求,而且所有的错误都会被捕获到。
7. 我们可以用react-error-boundary替代吗
对于那些讨厌重新发明轮子的人,那些只是希望第三库来解决他们问题的人,[react-error-boundary](GitHub - bvaughn/react-error-boundary)确实是一个不错的方案。
8. 原文地址
https://www.developerway.com/posts/how-to-handle-errors-in-react