Eli.dev

ErrorBoundary Card

2025年11月27日

react

包含错误边界处理的通用卡片

解决什么问题

  • 拒绝白屏❌,优化页面使用体验😊
  • 使用该组件包裹的后,组件内部js代码报错将会被捕获,展示兜底内容
  • 项目稳定性建设

实现原理

使用React API getDerivedStateFromError捕获组件报错,并修改hasError状态为true,展示兜底内容。 注意:函数式组件中目前还没有与 static getDerivedStateFromError 直接等同的东西。如果你想避免创建类式组件,请像上面那样编写一个 ErrorBoundary 组件,并在整个应用程序中使用它。或者使用 react-error-boundary 包来执行此操作。

核心代码

“vite”: “^3.0.4” “react”: “^18.2.0”, “antd”: “^5.5.2” “styled-components”: “^6.1.19”

Card:

import React, { FC, useMemo } from "react";
import styled from "styled-components";
import classNames from "classnames";
import { Spin, Tooltip } from 'antd';
import { QuestionOutlined } from '@ant-design/icons';
import ErrorBoundary from "./ErrorBoundary";

interface TitleProps {
  showTipIcon?: boolean;
  tooltip?: React.ReactNode;
  content?: React.ReactNode;
  suffix?: React.ReactNode;
}

interface CardProps {
  title?: React.ReactNode | TitleProps;
  extra?: React.ReactNode;
  loading?: boolean;
  className?: string;
  style?: React.CSSProperties;
}

const CardWrapper = styled.div`
  box-sizing: border-box;
  padding: 24px;
  background-color: #fff;
  border-radius: 8px;
`;

const TitleContent = styled.div`
  display: inline-flex;
  font-size: 20px;
  line-height: 28px;
  font-weight: 500;
  color: #000;
`;

const Card: FC<CardProps> = ({ title, extra, loading = false, className, style, children }) => {
  const hasTitle = useMemo(() => Boolean(title), [title]);
  const hasExtra = useMemo(() => Boolean(extra), [extra]);

  return (
    <ErrorBoundary>
      <Spin spinning={loading}>
        <CardWrapper className={className} style={style}>
          <div className="flex justify-between items-center gap-24">
            {hasTitle && typeof title === "object" && "content" in title && (
              <div className="flex justify-between items-center">
                {title.showTipIcon && <QuestionOutlined />}
                {"tooltip" in title ? (
                  <Tooltip content={title.tooltip}>
                    <TitleContent>{title.content}</TitleContent>
                  </Tooltip>
                ) : (
                  <TitleContent>{title.content}</TitleContent>
                )}
                {title.suffix && <div className="ml-8">{title.suffix}</div>}
              </div>
            )}
            {hasTitle && typeof title !== "object" && <TitleContent>{title}</TitleContent>}
            {hasExtra && <div>{extra}</div>}
          </div>
          <div className={classNames({ "mt-20": hasTitle || hasExtra })}>{children}</div>
        </CardWrapper>
      </Spin>
    </ErrorBoundary>
  );
};

export default Card;

ErrorBoundary:

import React from "react";
import { Empty } from 'antd';

interface ErrorBoundaryState {
  hasError: boolean;
}

interface ErrorBoundaryProps {
  children: React.ReactNode;
  fallback?: React.ReactNode;
}

class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
  state: ErrorBoundaryState = { hasError: false };

  constructor(props: ErrorBoundaryProps) {
    super(props);
  }

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

  componentDidCatch(error, errorInfo) {
    console.warn("ErrorBoundary caught an error", error, errorInfo);
    // TOTO: 手动上报到监控服务器
  }

  render() {
    if (this.state.hasError) {
      return (
        this.props.fallback || (
          <div className="rounded-lg bg-white p-8">
            <Empty description="组件内部发生了错误,请联系管理员" />
          </div>
        )
      );
    }
    return this.props.children;
  }
}

export default ErrorBoundary;

如何使用

function addComment(comment?) {
  if (comment == null) {
    throw new Error("Example Error: An error thrown to trigger error boundary");
  }
}

function AddCommentButton() {
  useEffect(() => {
    // 被ErrorBoundary捕获😊
    console.log(good.bbbb);
    // 计时器报错不会被ErrorBoundary捕获🙁
    const timer = setTimeout(() => {
      console.log(good.aaaa);
    }, 3000);
    return () => clearTimeout(timer);
  }, []);
  return (
    <button
      onClick={() => {
        // 事件不会被ErrorBoundary捕获🙁
        addComment();
      }}
    >
      Add comment
    </button>
  );
}


<div className="p-2 bg-black">
  <Cardtitle="Hello world">
    <AddCommentButton />
  </Card>
</div>

效果

效果

总结

错误类型捕获方式是否被 ErrorBoundary 捕获
同步渲染错误React 自动捕获
生命周期钩子错误React 自动捕获
异步代码错误手动 try-catch + useState
事件处理函数错误手动 try-catch