第十六章 React中常用的的错误处理方法 【下】

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库。
相关推荐
午后书香4 分钟前
一天三场面试,口干舌燥要晕倒(二)
前端·javascript·面试
Book_熬夜!19 分钟前
CSS—补充:CSS计数器、单位、@media媒体查询
前端·css·html·媒体
程序员大澈20 分钟前
1个基于 Three.js 的 Vue3 组件库
javascript·vue.js
程序员大澈26 分钟前
3个 Vue Scoped 的核心原理
javascript·vue.js
hyyyyy!29 分钟前
《原型链的故事:JavaScript 对象模型的秘密》
javascript·原型模式
程序员大澈39 分钟前
3个好玩且免费的api接口
javascript·vue.js
程序员大澈1 小时前
4个 Vue 路由实现的过程
javascript·vue.js·uni-app
几度泥的菜花1 小时前
如何禁用移动端页面的多点触控和手势缩放
前端·javascript
狼性书生1 小时前
electron + vue3 + vite 渲染进程到主进程的双向通信
前端·javascript·electron
肥肠可耐的西西公主1 小时前
前端(AJAX)学习笔记(CLASS 4):进阶
前端·笔记·学习