问题:一个组件内部有很多个子组件,其中一个出错,怎么实现其他组件可以正常显示,而不是页面挂掉?
可以捕获发生在其子组件树任何位置的 JavaScript 错误,并打印这些错误,同时展示降级 UI,错误边界可以捕获发生在整个子组件树的渲染期间、生命周期方法以及构造函数中的错误。
错误边界无法捕获以下场景中产生的错误:
setTimeout 或 requestAnimationFrame 回调函数)错误边界的工作方式类似于 JavaScript 的 catch {},不同的地方在于错误边界只针对 React 组件。只有 class 组件才可以成为错误边界组件。大多数情况下, 你只需要声明一次错误边界组件, 并在整个应用中使用它。
注意错误边界仅可以捕获其子组件的错误,它无法捕获其自身的错误。如果一个错误边界无法渲染错误信息,则错误会冒泡至最近的上层错误边界,这也类似于 JavaScript 中 catch {} 的工作机制。
如果一个 class 组件中定义了 static getDerivedStateFromError() 或 componentDidCatch() 这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,请使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息。
错误边界的粒度由你来决定,可以将其包装在最顶层的路由组件并为用户展示一个 “Something went wrong” 的错误信息,就像服务端框架经常处理崩溃一样。你也可以将单独的部件包装在错误边界以保护应用其他部分不崩溃。
static getDerivedStateFromError(error)
此生命周期会在后代组件抛出错误后被调用。 它将抛出的错误作为参数,并返回一个值以更新 state,在渲染阶段调用,因此不允许出现副作用。 如遇此类情况,请用 componentDidCatch()。
componentDidCatch(error, info)
此生命周期在后代组件抛出错误后被调用。 它接收两个参数:
error —— 抛出的错误。info —— 带有 componentStack key 的对象,其中包含有关组件引发错误的栈信息。在“提交”阶段被调用,因此允许执行副作用。
注意:如果发生错误,你可以通过调用 setState 使用 componentDidCatch() 渲染降级 UI,但在未来的版本中将不推荐这样做。 可以使用静态 getDerivedStateFromError() 来处理降级渲染。
1、基本使用
如下:若是没有ErrorBoundary组件,则组件内部报错整个页面会挂掉, 最顶层使用ErrorBoundary,那么一个组件报错整个页面UI会降级显示,若是每个子组件都包裹一层ErrorBoundary,那么一个组件出错,其他可以正常显示,出错的那个组件位置显示降级UI除非return null什么都不显示
- import ErrorBoundary from "./components/ErrorBoundary";
- import Child1 from "./test/Child1";
- import Child2 from "./test/Child2";
- import Child3 from "./test/Child3";
-
- const Child = function () {
- return (
- <ErrorBoundary>
- <Child1 />
- ErrorBoundary>
- );
- };
-
- //父组件中含多个子组件,若一个组件内部出问题,其他组件可以正常显示=》每个子组件包括一层ErrorBoundary进行UI降级或直接return null
- function App() {
- return (
- <div className="App">
- <ErrorBoundary>
- <Child />
- {/* <Child1 /> */}
- <Child2 />
- <Child3 />
- ErrorBoundary>
- div>
- );
- }
-
- export default App;
- const d: any = {};
- const Child1 = memo((props) => {
- console.log(d.d.y);
- return <p>this is Child1p>;
- });
- export default Child1;
-
- const Child2 = (props) => {
- const [count, setCount] = useState
(0); -
- return (
- <div>
- <p>this is Child2p>
- <p>count:{count}p>
- <button onClick={() => setCount((prev) => prev + 1)}>click mebutton>
- div>
- );
- };
- export default Child2;
-
-
- const Child3 = (props) => {
- return <p>this is Child3p>;
- };
- export default Child3;
- import React from "react";
-
- interface Props {
- children: React.ReactNode; //ReactElement只能一个根元素 多个用ReactNode
- }
- interface State {
- hasError: boolean;
- }
-
- class ErrorBoundary extends React.Component<Props, State> {
- constructor(props: Props) {
- super(props);
- this.state = { hasError: false };
- }
-
- static getDerivedStateFromError(error: string) {
- // 更新 state 使下一次渲染能够显示降级后的 UI
- return { hasError: true };
- }
-
- componentDidCatch(error: any, errorInfo: any) {
- // 你同样可以将错误日志上报给服务器
- // logErrorToMyService(error, errorInfo);
- console.log("componentDidCatch: ", error, errorInfo);
- }
-
- render() {
- if (this.state.hasError) {
- // 你可以自定义降级后的 UI 并渲染
- // return null
- return <h1>Something went wrong.h1>;
- }
-
- return this.props.children;
- }
- }
-
- export default ErrorBoundary;
2、可配置的错误边界
将日志上报的方法以及显示的 UI 通过接受传参的方式进行动态配置,对于传入的UI,我们可以设置以react组件的方式 或 是一个React Element进行接受,而且通过组件的话,我们可以传入参数,这样可以在兜底 UI 中拿到具体的错误信息。
- import React from "react";
-
- interface FallbackRenderProps {
- error: Error;
- }
- interface Props {
- children: React.ReactNode; //ReactElement只能一个根元素 多个用ReactNode
- onError?: (error: Error, errorInfo: string) => void;
- fallback?: React.ReactElement;
- FallbackComponent?: React.FunctionComponent<FallbackRenderProps>;
- }
- interface State {
- hasError: boolean;
- error: Error | null;
- }
-
- class ErrorBoundary extends React.Component<Props, State> {
- constructor(props: Props) {
- super(props);
- this.state = { hasError: false, error: null};
- }
-
- static getDerivedStateFromError(error: Error) {
- // 更新 state 使下一次渲染能够显示降级后的 UI
- return { hasError: true, error };
- }
-
- componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
- if (this.props.onError) {
- //上报日志通过父组件注入的函数进行执行
- this.props.onError(error, errorInfo.componentStack);
- }
- }
-
- render() {
- const { fallback, FallbackComponent } = this.props;
- const { error } = this.state;
- if (error) {
- const fallbackProps = { error };
- //判断是否为React Element
- if (React.isValidElement(fallback)) {
- return fallback;
- }
- //组件方式传入
- if (FallbackComponent) {
- return <FallbackComponent {...fallbackProps} />;
- }
-
- throw new Error("ErrorBoundary 组件需要传入兜底UI");
- }
- return this.props.children;
- }
- }
-
- export default ErrorBoundary;
使用:
- import ErrorBoundary from "./components/ErrorBoundary";
- import ErrorBoundaryWithConfig from "./components/ErrorBoundaryWithConfig";
- import Child1 from "./test/ErrorTest/Child1";
- import Child2 from "./test/ErrorTest/Child2";
- import Child3 from "./test/ErrorTest/Child3";
-
- interface IErrorUIprops {
- error: Error;
- }
- const ErrorUI: React.FC<IErrorUIprops> = ({ error }) => {
- return (
- <div>
- <p>出错了....p>
- <p>
- 错误信息:
- {JSON.stringify(error, ["message", "arguments", "type", "name"])}
- p>
- div>
- );
- };
-
- const Child = function () {
- const onError = (error: Error, errorInfo: string) => {
- console.log("Child error ", error);
- console.log("Child errorInfo ", errorInfo);
- };
-
- return (
- <ErrorBoundaryWithConfig onError={onError} FallbackComponent={ErrorUI}>
- <Child1 />
- ErrorBoundaryWithConfig>
- );
- };
-
-
- function App() {
- return (
- <div className="App">
- <ErrorBoundary>
- <Child />
- {/* <Child1 /> */}
- <Child2 />
- <ErrorBoundaryWithConfig fallback={<p>出错了....p>}>
- <Child3 />
- ErrorBoundaryWithConfig>
- ErrorBoundary>
- div>
- );
- }
-
- export default App;
进一步优化:有时候会遇到这种情况:服务器突然 503、502 了,前端获取不到响应,这时候某个组件报错了,但是过一会又正常了。比较好的方法是用户点一下被ErrorBoundary封装的组件中的一个方法来重新加载出错组件,不需要重刷页面,这时候需要兜底的组件中应该暴露出一个方法供ErrorBoundary进行处理。

- import React from "react";
-
- interface FallbackRenderProps {
- error: Error;
- resetErrorBoundary?: () => void;
- }
-
- interface Props {
- children: React.ReactNode; //ReactElement只能一个根元素 多个用ReactNode
- onError?: (error: Error, errorInfo: string) => void;
- fallback?: React.ReactElement;
- FallbackComponent?: React.FunctionComponent<FallbackRenderProps>;
- onReset?: () => void;
- fallbackRender?: (
- fallbackRenderProps: FallbackRenderProps
- ) => React.ReactElement;
- }
- interface State {
- hasError: boolean;
- error: Error | null;
- }
-
- class ErrorBoundary extends React.Component<Props, State> {
- constructor(props: Props) {
- super(props);
- this.state = { hasError: false, error: null };
- }
-
- static getDerivedStateFromError(error: Error) {
- // 更新 state 使下一次渲染能够显示降级后的 UI
- return { hasError: true, error };
- }
-
- componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
- if (this.props.onError) {
- //上报日志通过父组件注入的函数进行执行
- this.props.onError(error, errorInfo.componentStack);
- }
- }
-
- resetErrorBoundary = () => {
- if (this.props.onReset) this.props.onReset();
- this.setState({ hasError: false, error: null });
- };
-
- render() {
- const { fallback, FallbackComponent, fallbackRender } = this.props;
- const { error } = this.state;
- if (error) {
- const fallbackProps = {
- error,
- resetErrorBoundary: this.resetErrorBoundary,
- };
-
- //判断是否为React Element
- if (React.isValidElement(fallback)) {
- return fallback;
- }
-
- //函数方式传入
- if (typeof fallbackRender === "function") {
- return fallbackRender(fallbackProps);
- }
-
- //组件方式传入
- if (FallbackComponent) {
- return <FallbackComponent {...fallbackProps} />;
- }
-
- throw new Error("ErrorBoundary 组件需要传入兜底UI");
- }
-
- return this.props.children;
- }
- }
-
- export default ErrorBoundary;
如上:正常是显示children,children里面报错就会被捕获到,之后进行UI降级, 重置也是使其显示children,若是没有错误了那么正常显示,若是还是有错误还会被捕获到。
- import { useState } from "react";
- import ErrorBoundary from "./components/ErrorBoundary";
- import ErrorBoundaryWithConfig from "./components/ErrorBoundaryWithConfig";
- // import Home from "./test/home";
- import Child1 from "./test/ErrorTest/Child1";
- import Child2 from "./test/ErrorTest/Child2";
- import Child3 from "./test/ErrorTest/Child3";
-
- interface IErrorUIprops {
- error: Error;
- resetErrorBoundary?: () => void;
- }
- const ErrorUI: React.FC<IErrorUIprops> = ({ error, resetErrorBoundary }) => {
- return (
- <div>
- <p>出错了....p>
- <p>
- 错误信息:
- {JSON.stringify(error, ["message", "arguments", "type", "name"])}
- p>
- {resetErrorBoundary && (
- <button onClick={resetErrorBoundary}>Try againbutton>
- )}
- div>
- );
- };
-
- function App() {
- const [count, setCount] = useState(0);
-
- const onReset = () => setCount(0); //点击重置时进行的回调
-
- const onError = (error: Error, errorInfo: string) => {
- console.log("Child error ", error);
- console.log("Child errorInfo ", errorInfo);
- };
-
- // fallback 组件的渲染函数
- const renderFallback = (props: IErrorUIprops) => {
- return <ErrorUI {...props} />;
- };
-
- return (
- <div className="App">
- <ErrorBoundary>
- <section>
- <button onClick={() => setCount((count) => count + 1)}>+button>
- <button onClick={() => setCount((count) => count - 1)}>-button>
- section>
- <hr />
- {/* <Child1 /> */}
- <ErrorBoundaryWithConfig
- onError={onError}
- onReset={onReset}
- fallbackRender={renderFallback}
- /*FallbackComponent={ErrorUI}*/
- >
- <Child1 count={count} />
- ErrorBoundaryWithConfig>
-
- <Child2 />
- <ErrorBoundaryWithConfig fallback={<p>出错了....p>}>
- <Child3 count={count} />
- ErrorBoundaryWithConfig>
- ErrorBoundary>
- div>
- );
- }
-
- export default App;
注意:点击+,当达到2时Child1报错( if (count === 2) throw new Error("count is two");),UI降级,继续点击+,为3时Child3报错(if (count === 3) throw new Error("count is three");)UI降级,此时Child1有重置,点击重置按钮onReset把count重置为0了,Child1 UI也重置了显示正常,而Child3之前显示了降级UI,没有重置或不刷新,页面即使数据正常了UI不会更新,还是降级UI,所以重置按钮在不刷新页面情况下可以解决此类问题。
局限性:触发重置的动作只能在 fallback 里面。假如我的重置按钮不在 fallback 里呢?或者 onReset 函数根本不在这个 App 组件下那怎么办呢?难道要将 onReset 像传家宝一路传到这个 App 再传入 ErrorBoundary 里?
思路1:能不能监听状态的更新,只要状态更新就重置,反正就重新加载组件也没什么损失,这里的状态完全用全局状态管理,放到 Redux 中。
思路2:上面的思路听起来不就和 useEffect 里的依赖项 deps 数组一样嘛,不妨在 props 提供一个 resetKeys 数组,如果这个数组里的东西变了,ErrorBoundary 就重置,这样一控制是否要重置就更灵活了。
假如是由于网络波动引发的异常,那页面当然会显示 fallback 了,如果用上面直接调用 props.resetErrorBoundary 方法来重置,只要用户不点“重置”按钮,那块地方永远不会被重置。又由于是因为网络波动引发的异常,有可能就那0.001 秒有问题,别的时间又好了,所以如果我们将一些变化频繁的值放到 resetKeys 里就很容易自动触发重置。例如,报错后,其它地方的值变了从而更改了 resetKeys 的元素值就会触发自动重置。对于用户来说,最多只会看到一闪而过的 fallback,然后那块地方又正常了。
- // 本组件 ErrorBoundary 的 props
- interface Props{
- ...
- resetKeys?: Array
; - onResetKeysChange?: (
- prevResetKey: Array
| undefined, - resetKeys: Array
| undefined, - ) => void;
- }
-
- // 检查 resetKeys 是否有变化
- const changedArray = (a: Array
= [], b: Array = [] ) => { - return (
- a.length !== b.length || a.some((item, index) => !Object.is(item, b[index]))
- );
- };
-
- class ErrorBoundary extends React.Component<Props, State> {
- ...
-
- componentDidUpdate(prevProps: Readonly
> ) { - const { resetKeys, onResetKeysChange } = this.props;
-
- // 只要 resetKeys 有变化,直接 reset
- if (changedArray(prevProps.resetKeys, resetKeys)) {
- if (onResetKeysChange) {
- onResetKeysChange(prevProps.resetKeys, resetKeys);
- }
-
- // 重置 ErrorBoundary 状态,并调用 onReset 回调
- this.reset();
- }
- }
-
- resetErrorBoundary = () => {
- if (this.props.onReset) this.props.onReset();
- this.reset();
- };
-
- reset = () => {
- this.setState({ hasError: false, error: null });
- };
-
- render() {
- ...
- }
- }
上面存在问题:假如某个 key 是触发 error 的元凶,那么就有可能触发二次 error 的情况:
xxxKey 触发了 error,组件报错resetKeys 里的一些东西改了componentDidUpdate 发现 resetKeys 里有东西更新了,不废话,马上重置如下:假如接口请求失败导致组件报错即xxkey触发组件错误:render渲染children,children报错被getDerivedStateFromError等捕获UI降级,捕获到错误后重新请求导致resetKeys里面的请求状态又发生改变,componentDidUpdate就会重置,重置后组件还是报错,就会出现如上循环。=》包括下面案例只是简单举例便于理解,应用场景不符合
- componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
- if (this.props.onError) {
- //上报日志通过父组件注入的函数进行执行
- this.props.onError(error, errorInfo.componentStack);
- }
- }
-
-
- const onError = (error: Error, errorInfo: string) => {
- setCount(Math.random() * 3);
- };
-
-
- const Child1: FC<Props> = memo(({ count }) => {
- if (count < 3) throw new Error("count is two");
-
- return <p>this is Child1p>;
- });
-
- export default Child1;

这样的情况下就会被该ErrorBoundary的上层错误边界捕获,导致整体UI降级。

优化:有错误才重置,且不是因为错误导致后续连续
- componentDidUpdate(prevProps: Readonly
>, preState: State ) { - const {error} = this.state;
- const {resetKeys, onResetKeysChange} = this.props;
-
- // 已经存在错误,并且是第一次由于 error 而引发的 render/update,那么设置 flag=true,不会重置
- if (error !== null && !this.updatedWithError) {
- this.updatedWithError = true;
- return;
- }
-
- // 已经存在错误,并且是普通的组件 render,则检查 resetKeys 是否有改动,改了就重置
- if (error !== null && preState.error !== null && changedArray(prevProps.resetKeys, resetKeys)) {
- if (onResetKeysChange) {
- onResetKeysChange(prevProps.resetKeys, resetKeys);
- }
-
- this.reset();
- }
- }
updatedWithError 作为 flag 判断是否已经由于 error 出现而引发的 render/updateupdatedWithError = true,不会重置状态updatedWithError 为 true 说明已经由于 error 而更新过了,以后的更新只要 resetKeys 里的东西改了,都会被重置简单案例:
- function App() {
- const [explode, setExplode] = React.useState(false)
-
- return (
- <div>
- <button onClick={() => setExplode(e => !e)}>toggle explodebutton>
- <ErrorBoundary
- FallbackComponent={ErrorFallback}
- onReset={() => setExplode(false)}
- resetKeys={[explode]}
- >
- {explode ? <Bomb /> : null}
- ErrorBoundary>
- div>
- )
- }
注意执行逻辑:
初次渲染,执行render,渲染children报错,getDerivedStateFromError捕获错误,导致state变化,重新执行render,UI降级,初次渲染不执行componentDidUpdate。在错误捕获时执行了componentDidCatch,导致resetkey变化,props变化重新执行render,遇到判断error存在还是显示降级UI,那么就不不会再执行getDerivedStateFromError和componentDidCatch,但是props变化了会执行componentDidUpdate,在这里判断若是由于出错导致更新跳过重置=》整体就是出错导致UI降级。
非初次渲染:如初次渲染页面正常,父组件某些操作导致状态变化影响到resetkey变化,props改变执行render,渲染children报错,getDerivedStateFromError捕获错误,导致state变化,重新执行render,UI降级,但此时不是初次渲染props改变要执行componentDidUpdate,在上面判断了判断若是由于出错导致更新跳过重置。但是componentDidCatch执行时又改变resetkey,props改变执行render,此时error存在直接显示降级UI不会再触发getDerivedStateFromError和componentDidCatch,但是props变化了要执行componentDidUpdate,此时已经不是错误导致的更新,componentDidUpdate执行重置(保证重置没有问题否则又会从开始非初次渲染循环,此时可以在componentDidCatch设置this.updatedWithError = false,但是这样就没有意义了)
在 componentDidUpdate 里,只要不是由于 error 引发的组件渲染或更新,而且 resetKeys 有变化了,那么直接重置组件状态来达到自动重置=》只适用于某些场景,使用时注意。
至此,我们拥有了两种可以实现重置的方式了:
| 方法 | 触发范围 | 使用场景 | 思想负担 |
|---|---|---|---|
| 手动调用 resetErrorBoundary | 一般在 fallback 组件里 | 用户可以在 fallback 里手动点击“重置”实现重置 | 最直接,思想负担较轻 |
| 更新 resetKeys | 哪里都行,范围更广 | 用户可以在报错组件外部重置、resetKeys 里有报错组件依赖的数据、渲染时自动重置 | 间接触发,要思考哪些值放到 resetKeys 里,思想负担较重 |
以上ErrorBoundary的使用可以整体封装成HOC:
- import ErrorBoundary from "../components/ErrorBoundaryWithConfig";
- import { Props as ErrorBoundaryProps } from "../components/ErrorBoundaryWithConfig";
-
- /**
- * with 写法
- * @param Component 业务组件
- * @param errorBoundaryProps error boundary 的 props
- */
-
- function withErrorBoundary
(
- Component: React.ComponentType
,
- errorBoundaryProps: ErrorBoundaryProps
- ): React.ComponentType
{
- const Wrapped: React.ComponentType
= (props) => {
- return (
- <ErrorBoundary {...errorBoundaryProps}>
- <Component {...props} />
- ErrorBoundary>
- );
- };
-
- // DevTools 显示的组件名
- const name = Component.displayName || Component.name || "Unknown";
- Wrapped.displayName = `withErrorBoundary(${name})`;
-
- return Wrapped;
- }
-
- export default withErrorBoundary;
在使用错误边界组件处理普通组件时,错误边界无法捕获异步代码、服务端错误、事件内部错误以及自己错误,所以遇到这种情况可以使用try catch或者异步操作自身的catch捕获,或者直接抛出异常,封装如下:
- function useErrorHandler(givenError?: unknown): (error: unknown) => void {
- const [error, setError] = React.useState
(null) - if (givenError != null) throw givenError
- if (error != null) throw error
- return setError
- }
使用:
- import { useErrorHandler } from 'react-error-boundary'
-
- function Greeting() {
- const [greeting, setGreeting] = React.useState(null)
- const handleError = useErrorHandler()
-
- function handleSubmit(event) {
- event.preventDefault()
- const name = event.target.elements.name.value
- fetchGreeting(name).then(
- newGreeting => setGreeting(newGreeting),
- handleError,
- )
- }
-
- return greeting ? (
- <div>{greeting}div>
- ) : (
- <form onSubmit={handleSubmit}>
- <label>Namelabel>
- <input id="name" />
- <button type="submit">get a greetingbutton>
- form>
- )
- }
参考:
第三方库:react-error-boundary
GitHub - haixiangyan/my-react-error-bounday: 手把手教你实现 react-error-boundary