第十六章 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 分钟前
03-HTML常见元素
前端·html
kidding72310 分钟前
gitee新的仓库,Vscode创建新的分支详细步骤
前端·gitee·在仓库创建新的分支
听风吹等浪起14 分钟前
基于html实现的课题随机点名
前端·html
leluckys19 分钟前
flutter 专题 六十三 Flutter入门与实战作者:xiangzhihong8Fluter 应用调试
前端·javascript·flutter
kidding72333 分钟前
微信小程序怎么分包步骤(包括怎么主包跳转到分包)
前端·微信小程序·前端开发·分包·wx.navigateto·subpackages
微学AI1 小时前
详细介绍:MCP(大模型上下文协议)的架构与组件,以及MCP的开发实践
前端·人工智能·深度学习·架构·llm·mcp
liangshanbo12151 小时前
CSS 包含块
前端·css
Mitchell_C1 小时前
语义化 HTML (Semantic HTML)
前端·html
倒霉男孩1 小时前
CSS文本属性
前端·css
shoa_top1 小时前
JavaScript 数组方法总结
javascript