React ErrorBoundary
🎗️

React ErrorBoundary

Tags
React
错误处理
description
更新时间
Last updated June 1, 2022
 

什么是ErrorBoundary

我们在使用React开发程序的时候难免会遇到代码错误,比如从一个undefined的对象上访问值
import './App.css'; function App() { const a = undefined; const handleClick = () =>{ console.log(a.test) } return ( <button onClick={handleClick}>test</button> ); } export default App;
notion image
如果这种错误仅仅发生在事件的回调函数中,那对我们的组件渲染是没有任何影响的,我们可能会在控制台上看到对应的报错。
notion image
但是如果我们的错误是在返回的渲染树上呢。
import './App.css'; function App() { const a = undefined; const handleClick = () =>{ console.log(a.test) } return ( <button onClick={handleClick}>test: {a.test}</button> ); } export default App;
结果就是我们不仅会看到控制台的报错,并且整个按钮都看不见了。

ErrorBoundary的具体表现是什么

我们可以进一步探索当渲染树的某一环报错会导致什么影响
import './App.css'; function Child(){ const a = undefined; return <span>test: {a.test}</span> } function App() { const a = undefined; const handleClick = () =>{ console.log(a.test) } return ( <> <button onClick={handleClick}>test: </button> <Child></Child> </> ); } export default App;
在上面的代码中,只有Child组件引入了一个渲染树上的错误,但是当我们查看渲染结果时,会发现整个组件树都没有显示。页面是一片空白的。
notion image
那么这就说明一个问题,当我们有一个庞大的应用,而应用中的某一个子组件,可能是一个很小的组件的渲染出错时,就会导致整个渲染树都被卸载。换句话说,整个应用都会崩掉,必须刷新才能使用。
 
React对此的解释是
💡
这一改变具有重要意义,自 React 16 起,任何未被错误边界捕获的错误将会导致整个 React 组件树被卸载。 我们对这一决定有过一些争论,但根据我们的经验,把一个错误的 UI 留在那比完全移除它要更糟糕。例如,在类似 Messenger 的产品中,把一个异常的 UI 展示给用户可能会导致用户将信息错发给别人。同样,对于支付类应用而言,显示错误的金额也比不呈现任何内容更糟糕。 此变化意味着当你迁移到 React 16 时,你可能会发现一些已存在你应用中但未曾注意到的崩溃。增加错误边界能够让你在应用发生异常时提供更好的用户体验。 例如,Facebook Messenger 将侧边栏、信息面板、聊天记录以及信息输入框包装在单独的错误边界中。如果其中的某些 UI 组件崩溃,其余部分仍然能够交互。 我们也鼓励使用 JS 错误报告服务(或自行构建),这样你能了解关于生产环境中出现的未捕获异常,并将其修复。
 
 
所以相对于整个渲染树都被移除,我们希望至少:
  1. 显示一个全局的错误页面,告诉用户当前应用遇到了一个错误,显示错误码或者错误原因用于反馈错误
  1. 将错误限制在某一个范围之内,比如某一个子树范围内展示错误,其他的子树不受影响。
 
于是就有了错误边界(ErrorBoundary)。
错误边界是一种 React 组件,这种组件可以捕获发生在其子组件树任何位置的 JavaScript 错误,并打印这些错误,同时展示降级 UI ,而并不会渲染那些发生崩溃的子组件树。错误边界可以捕获发生在整个子组件树的渲染期间、生命周期方法以及构造函数中的错误。
 
如下所示
import React from 'react' import './App.css'; class ErrorBoundary extends React.Component { .... render() { if (this.state.hasError) { // 你可以自定义降级后的 UI 并渲染 return <h1 style={{background: 'red'}}>Something went wrong.</h1>; } return this.props.children; } } function Child(){ const a = undefined; return <span>test: {a.test}</span> } function App() { const a = undefined; const handleClick = () =>{ console.log(a.test) } return ( <> <button onClick={handleClick}>test</button> <ErrorBoundary> <Child></Child> </ErrorBoundary> </> ); } export default App;
当Child组件被ErrorBoundary组件包住的时候,Child组件的错误就会被捕获并且不会影响更上一级的渲染树了。并且ErrorBoundary会降级渲染对应的UI。
notion image
 

ErrorBoundary 如何定义

如果一个 class 组件中定义了 static getDerivedStateFromError()  或 componentDidCatch()  这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,请使用 static getDerivedStateFromError()  渲染备用 UI ,使用 componentDidCatch()  打印错误信息。
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // 更新 state 使下一次渲染能够显示降级后的 UI return { hasError: true }; } componentDidCatch(error, errorInfo) { // 你同样可以将错误日志上报给服务器 console.log(error, errorInfo); } render() { if (this.state.hasError) { // 你可以自定义降级后的 UI 并渲染 return <h1 style={{background: 'red'}}>Something went wrong.</h1>; } return this.props.children; } }
上面的ErrorBoundary会根据state中的hasError来判断是渲染降级后的UI还是对应的children(原本应该渲染的UI)
 
getDerivedStateFromErrorcomponentDidCatch 都会在后代组件抛出错误后被调用,componentDidCatch “提交”阶段被调用,所以允许执行副作用,一般用来记录错误信息,而 getDerivedStateFromError 则用来改变对应的错误边界的状态,控制是否展示children
 

重新加载

有了ErrorBoundary,错误的组件可以由ErrorBoundary控制,那我们也可以让他被重新加载
import React, { useEffect, useState } from "react"; import "./App.css"; class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } reload() { this.setState({ hasError: false, }); } static getDerivedStateFromError(error) { // 更新 state 使下一次渲染能够显示降级后的 UI return { hasError: true }; } componentDidCatch(error, errorInfo) { // 你同样可以将错误日志上报给服务器 console.log(error, errorInfo); } render() { if (this.state.hasError) { // 你可以自定义降级后的 UI 并渲染 return ( <div> <h1 style={{ background: "red" }}>Something went wrong.</h1> <button onClick={() => this.reload()}>重新加载</button> </div> ); } return this.props.children; } } function Child() { const [a, setA] = useState({ test: 1, }); useEffect(() => { setTimeout(() => { setA(undefined); }, 5000); }, []); return <span>test: {a.test}</span>; } function App() { const a = undefined; const handleClick = () => { console.log(a.test); } return ( <> <button onClick={handleClick}>test</button> <ErrorBoundary> <Child></Child> </ErrorBoundary> </> ); } export default App;
notion image
我们可以在ErrorBoundary中提供一个重新加载的按钮,点击之后将hasError变为false,触发子组件的重新加载,如果只是因为某些错误的操作导致的子组件出错,那用户至少不用刷新整个页面就能重新加载对应的部分并重新进行正确的操作。
 
 
注意
错误边界 无法 捕获如下错误:
  • 事件处理
  • 异步代码 (例如 setTimeout 或 requestAnimationFrame 回调函数)
  • 服务端渲染
  • 错误边界自身抛出来的错误 (而不是其子组件)
 
参考文档