React Error Boundary 导致路由失效

1,008 阅读1分钟

出发点

React ErrorBoundary 作用: 防止页面崩溃

在使用错误边界组件的时候,我发现路由失效了,在此分享我的解决方案

问题复现

ErrorBoundary 组件使用 componentDidCatch 生命周期捕获错误,因此只能使用 class 组件

import React from 'react'
import { BrowserRouter, Link, Routes, Route } from "react-router-dom";

import Homepage from './Homepage';
import PageWithError from './PageWithError';

export default function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>{" "}
        <Link to="/page-with-error">Broken Page</Link>
      </nav>
      <ErrorBoundary>
        <Routes>
          <Route path="/" element={<Homepage />} />
          <Route path="/page-with-error" element={<PageWithError />} />
        </Routes>
      </ErrorBoundary>
    </BrowserRouter>
  );
}

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  render() {
    return this.state.hasError
      ? <h1>Something went wrong.</h1>
      : this.props.children; 
  }
}

如此使用后,再次进行路由跳转,发现路由全部失效了。

解决方案

在网上兜兜转转,终于找到了原因: 在捕获错误后,组件的 hasError 依旧为 true,导致路由组件没有被渲染 解决方法是,在外面再包裹一个组件,用以在 ErrorBoundary 组件重渲染的时候渲染路由组件。

import type { Dispatch, ErrorInfo, FC, ReactNode, SetStateAction } from 'react';
import { Component, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';

interface PropsType {
  children: ReactNode;
  hasError: boolean;
  ErrorComponent?: FC<StateType>;
  setHasError: Dispatch<SetStateAction<boolean>>;
}

interface StateType {
  hasError: boolean;
  error: null | Error;
  errorInfo: null | ErrorInfo;
}

class ErrorBoundaryInner extends Component<PropsType, StateType> {
  constructor(props: PropsType) {
    super(props);
    this.state = {
      hasError: false,
      error: null,
      errorInfo: null,
    };
  }

  componentDidUpdate(prevProps: PropsType) {
    if (!this.props.hasError && prevProps.hasError) {
      this.setState({ hasError: false });
    }
  }

  // 捕获抛出异常
  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    this.props.setHasError(true);
    // 传递异常信息
    this.setState({
      hasError: true,
      error,
      errorInfo,
    });
  }

  render() {
    // 如果捕获到异常,渲染降级UI
    if (this.state.hasError) {
      return (
        this.props.ErrorComponent?.(this.state) || (
          <div>
            <h1>{`Error:${this.state.error?.message}`}</h1>
            <details style={{ whiteSpace: 'pre-wrap' }}>
              {this.state.errorInfo?.componentStack}
            </details>
          </div>
        )
      );
    }

    return this.props.children;
  }
}

const ErrorBoundary: FC<any> = ({ children }) => {
  const [hasError, setHasError] = useState(false);
  const location = useLocation();
  useEffect(() => {
    if (hasError) {
      setHasError(false);
    }
  }, [location.key]);
  return (
    <ErrorBoundaryInner hasError={hasError} setHasError={setHasError}>
      {children}
    </ErrorBoundaryInner>
  );
};

export default ErrorBoundary;

参考文章Error boundary causes React Router links to stop working