过去组件内的的js错误常常会破坏React内部的状态,并在下一次渲染时产生 加密的 错误信息。
这些错误总会早应用代码的早期触发,但是React并没有能提供一种方式能够在组件内部优雅的处理,也不能从错误中恢复。
错误边界介绍
部分ui的异常,不应该破坏整个React应用,为了解决这个问题,React16引进了一种称为用户边界的新概念。
错误边界是用于 捕获其子组件树的js错误异常,记录错误并且展示一个回退的UI的React组件。而不是整个子组件树的异常,错误组件在渲染期间,生命周期方法内,以及整个组件树的构造函数内捕捉错误。
错误边界无法捕获如下错误:
- 事件处理 (了解更多)
- 异步代码 (例如
setTimeout
或requestAnimationFrame
回调函数) - 服务端渲染
- 错误边界自身抛出来的错误 (而不是其子组件)
如果一个类组件定义了一个名为`componentDidCatch(ERR,INFO)的方法。那么它成为了一个错误边界。
class ErrorBoundary extends React.Component{
constructor(props){
super(props);
this.state={hasError:false}
}
componentDidCatch(error,info){
this.setState({hasError:true});
logErrorToMyService(error,info);
}
render(){
if(this.state.hasError){
return <h1>somethings went wrong</h1>
}
return this.props.children;
}
}
//然后就可以像一个普通组件一样使用
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
componentDidCatch()
方法机制类似于 JavaScript catch {},但是针对组件。仅有类组件可以成为错误边界。实际上,大多数时间你仅想要定义一个错误边界组件并在你的整个应用中使用。
错误边界只能捕获 子组件的错误,它无法捕获自身的错误。如果一个错误边界无法渲染错误信息,那么错误边界会向上冒泡到最接近的错误边界。这也类似于 js 的catch 的工作机制。
componentDidCatch(err,info)参数
其中 ,err
是被抛出的参数。
info
是一个含有componentStack
的属性对象。这其中包含了组件在错误期间的信息。
//...
componentDidCatch(error, info) {
/* Example stack information:
in ComponentThatThrows (created by App)
in ErrorBoundary (created by App)
in div (created by App)
in App
*/
logComponentStackToMyService(info.componentStack);
}
//...
在线演示
查看通过 React 16 beta 版本来定义和使用错误边界的例子。
如何放置错误边界
错误边界的粒度完全取决于你的应用,可以将它包装在最顶层的路由组件,并且为用户展示一个发生异常的 的错误信息。就像服务端框架通常处理崩溃那样。你也可以将单独的插件包装在错误边界内部以保护应用不受该组件崩溃的影响。
未捕获错误(Uncaught Errors)的新行为
这个改变有着非常重要的意义。自 React 16 开始,任何未被错误边界捕获的错误将会卸载整个 React 组件树。
我们对这一决定饱含争论,但在我们的经验中放置下一个错误的UI比完全移除它要更糟糕。例如,在类似 Messenger 的产品中留下一个异常的可见 UI 可能会导致用户将信息发错给别人。类似的,对于支付类的应用来说,什么都不展示也比显示一堆错误更好。
这一改变意味着随着你迁入到 React 16,你将可能会发现一些已存在你应用中但未曾注意到的崩溃。增加错误边界能够让你在发生异常时提供更好的用户体验。
例如,Facebook Messenger 将侧边栏、信息面板,对话框以及信息输入框包装在单独的错误边界中。如果其中的某些 UI 组件崩溃,其余部分仍然能够交互。
我们也鼓励使用 JS 错误报告服务(或自行构建)这样你能够掌握在生产环境中发生的未捕获的异常,并将其修复。
组件栈追踪
React 16 会将渲染期间所有在开发环境下的发生的错误打印到控制台,即使应用程序意外的将其掩盖。除了错误信息和 JavaScript 栈外,其还提供了组件栈追踪。现在你可以准确地查看发生在组件树内的错误信息:
也可以在组件堆栈中查看文件名和行数。这一功能在 Create React App 项目中默认开启:
若你不使用 Create React App,你可以手动添加该插件到你的 Babel 配置中。注意其仅能在开发环境中使用并禁止在生产环境中使用。
为何不使用 try/catch?
try / catch 非常棒,但其仅能在命令式代码(imperative code)下可用:
然而,React 组件是声明式的并且具体指出 声明 什么需要被渲染:
<Button />
错误边界保留了 React 原生的声明性质,且其行为符合你的预期。例如,即使错误发生 componentDidUpdate 时期由某一个深层组件树中的 setState 调用引起,其仍然能够冒泡到最近的错误边界。
事件处理器如何处理?
错误边界无法捕获事件处理器内部的错误。
React 不需要错误边界在事件处理器内将其从错误中恢复。不像渲染方法或生命周期钩子,事件处理器不会再渲染周期内触发。因此若他们抛出异常,React 仍然能够知道需要在屏幕上显示什么。
如果你需要在事件处理器内部捕获错误,使用普通的 JavaScript try / catch 语句:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { error: null };
}
handleClick = () => {
try {
// Do something that could throw
} catch (error) {
this.setState({ error });
}
}
render() {
if (this.state.error) {
return <h1>Caught an error.</h1>
}
return <div onClick={this.handleClick}>Click Me</div>
}
}
注意上述例子仅是说明普通的 JavaScript 行为而并未使用错误边界。
自 React 15 的名称变更
React 15 在一个不同的方法名下:unstable_handleError
包含了一个支持有限的错误边界。这一方法不再能用,同时自 React 16 beta 发布起你需要在代码中将其修改为 componentDidCatch
。
为这一改变,我们已提供了一个 codemod 来帮助你自动迁移你的代码。