React的ErrorBoundary组件
为了避免上述的限制,React为我们提供了一个名为"Error Boundaries"的API,用了捕获React生命周期内的报错:
js
const Component = () => {
return (
<ErrorBoundary>
<SomeChildComponent />
<AnotherChildComponent />
</ErrorBoundary>
);
};
如此一来,ErrorBoundary
内的子组件渲染过程中的报错,都会被它捕获。
但 React 本身并没有直接提供给我们组件,它只是给了我们一个实现组件的工具。最简单的实现大概会是下面这样:
js
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;
}
}
我们声明里一个常规的类组件(类组件也许有些老学究,但确实没有钩子可以处理错误边界),并实现了getDerivedStateFromError
方法,如此一来,组件就具有识别错误边界的能力了。
在处理错误时,另一件重要的事情是将错误信息发送到某个地方,以便能提醒到所有处于待命状态的人员。为此,错误边界为我们提供了 componentDidCatch
方法:
js
class ErrorBoundary extends React.Component {
// everything else stays the same
componentDidCatch(error, errorInfo) {
// send error to somewhere here
log(error, errorInfo);
}
此外,我们可以设置fallback
属性,以便使用者可以在发生错误时,为用户提供更准确的信息:
js
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
然后这样使用:
js
const Component = () => {
return (
<ErrorBoundary fallback={<>Oh no! Do something!</>}>
<SomeChildComponent />
<AnotherChildComponent />
</ErrorBoundary>
);
};
代码示例: advanced-react.com/examples/16...
但要注意的是,即便如此,仍然无法捕获所有的错误
ErrorBoundary 组件的局限性
ErrorBoundary组件只能处理React生命周期内的报错。发生在React生命周期外的报错,比如已经完成的promises,setTimeout
内的异步代码,各种回调,甚至是事件处理器,ErrorBoundary组件是无法处理的。
js
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
。至少,我们可以安全地使用状态。所以,我们可以把这两种模式混合在一起使用:
js
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>
);
};
但是,如此一来,我们又回到原来那个问题了:要为每一个组件设置"error"状态,更重要的是,还要决定如何处理这些状态。
好在,我们可以不把错误处理放在子组件层面,而是通过属性或者 Context
把它们提升到父组件ErrorBoundary
层面":
js
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
组件捕获异步报错
有趣的是,我们可以使用ErrorBoundary
组件捕获所有的报错!有一个酷炫的技巧可以实现它!
而这个技巧是,首先用try/catch
来捕获错误。之后,在catch
对应的块里面,触发一个常规对待React重新渲染。如此一来,ErrorBoundary
组件就可以处理这些报错了。又因为状态更新函数会触发重新渲染,状态更新函数可以接受一个更新函数作为参数:
js
const Component = () => {
// create some random state that we'll use to throw errors
const [sate, setState] = useState();
const onClick = () => {
try {
// something bad happended
} catch (e) {
// trigger state update, with updater function as an argument
setState(() => {
// re-throw this error within updater fucntion
// it will be triggered during state update
throw e;
})
}
}
}
代码示例: advanced-react.com/examples/16...
最后,我们需要把这个逻辑抽象成一个钩子,这样就不用四处设置error状态了:
js
const useThrowAsyncError = () => {
const [state, setState] = useState();
return (error) => {
setState(() => throw error)
}
}
然后,这是该钩子的用例:
js
const Component = () => {
const throwAsyncError = useThrowAsyncError();
useEffect(() => {
fetch('/bla')
.then()
.catch((e) => {
// throw async error here !
throwAsyncError(e);
});
});
}
或者,我们可以把它和回调函数包装在一起:
js
const useCallbackWithErrorHandling = (callback) => {
const [state, setState] = useState();
return (...args) => {
try {
callback(...args);
} catch (e) {
setState(() => throw e);
}
}
}
然后这么使用:
js
const Component = () => {
const onClick = () => {
// do something dangerous here
}
const onClickWithErrorHandler =
useCallbackWithErrorHandling(onClick);
return (
<button onClick={onClickWithErrorHandler}>
click me!
</button>
)
}
代码示例: advanced-react.com/examples/16...
可不可以之用处理React报错的库?
对于那些讨厌重复造轮子,或者喜欢借助现成库来解决问题的人,有一个不错的库可以推荐,它是"react-error-boundary"。它实现了一个灵活的ErrorBoundary
组件,还提供了类似的工具函数来解决刚刚的问题。
知识概要:
这就是本章关于错误处理的内容了,这本书也到此为止了。希望阅读这本书,对你来说是一段愉快的体验。当然,在处理React中的错误时:
- 在React 16及以后的版本中,未被捕获的错误,会导致整个应用被卸载。所以,在关键位置设置错误处理边界是必要的。
try/catch
用来处理回调函数内,或者promise内的报错,是可以的。但是它无法处理嵌套组件内的报错,也不能用它来包裹整个useEffect
钩子。- 而
ErrorBoundary
组件则相反。它能够处理渲染树渲染过程中的报错,但它无法处理promises和回调内的报错。 - 当然,我们可以把
ErrorBoundary
组件和try/catch
组合在一起,这样,就可以使用try/catch
捕获异步回调过程中的错误,并通过setState把错误抛给ErrorBoundary
组件,使这个报错可以进入React生命周期。 - 为了实现这个抛错误的逻辑,我们可以实现一个简单的
useAsyncError
钩子,或者使用react-error-boundary
库。