You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

点击元素触发Maximum update depth exceeded错误的技术求助

问题:React组件触发「Maximum update depth exceeded」错误

ProgressMinimalIconsConnected是一款分步指引组件,移动端下每个步骤会显示tooltip,tooltip通过setTimeout自动消失,用户点击步骤可重新唤起tooltip。核心问题:当当前步骤的tooltip仍处于显示状态时,点击该步骤会触发「Maximum update depth exceeded」错误;tooltip消失后点击无此问题,且仅当前步骤会触发该错误。

相关代码

export type ProgressMinimalIconsConnectedProps = ProgressStepProps & { 
    connectorClassName?: string, 
    isTooltipOpen?: boolean, 
    tooltipDuration?: number 
};

export const ProgressMinimalIconsConnected = memo( forwardRef<HTMLDivElement, ProgressMinimalIconsConnectedProps>( ( { size = "sm", className, currentStep = 0, isTooltipOpen, tooltipDuration, connectorClassName, ...props }, ref, ) => { 
    const [ activeStep, setActiveStep ] = useState(0);
    const [ tooltipOpen, setTooltipOpen ] = useState(false); 
    const isMobileView = useIsMobileView();  

 useEffect(() => {
    setActiveStep(currentStep);
    if (isMobileView) {
      const isOpen = currentStep === 0 || currentStep !== -1;
      setTooltipOpen(isOpen);

      const time = setTimeout(() => {
        setTooltipOpen(false);
      }, tooltipDuration);

      return () => clearTimeout(time);
    }
  }, [ currentStep, isMobileView ]);

  const handleStepClick = (index: number) => {
    if (isMobileView) {
      setActiveStep(index);
      setTooltipOpen(true);
      const time = setTimeout(() => {
        setTooltipOpen(false);
      }, tooltipDuration);
      return () => clearTimeout(time);
    }
  };

  return (
    <div
      className={classnames(styles.root, className)}
      {...props}
      ref={ref}
    >
      {props.steps.map((item, index) => (
        <div className={styles.container} key={item.id}>
          <Tooltip
            content={item.text}
            supportingContent={item.supportingText}
            isOpen={isTooltipOpen && isMobileView && activeStep === index && tooltipOpen}
            placement="top"
          >
            <StepBase
              {...item}
              id={`${item?.id}`}
              size={size}
              type="iconOnly"
              status={getIndicatorState(index, currentStep)}
              onClick={() => handleStepClick(index)}
            />
          </Tooltip>
          {index < props.steps.length - 1 && (
            <div className={classnames(styles.connector, connectorClassName, {
              [styles.connectorComplete]: index < currentStep,
            })}></div>
          )}
        </div>
      ))}
    </div>
  );
},
), );

错误原因分析

  1. 状态更新循环触发:点击当前步骤时,handleStepClick中调用setActiveStep(index)(此时index等于currentStep),触发useEffect(依赖包含currentStep)重新执行,useEffect里又会调用setTooltipOpen(isOpen),和handleStepClick中的setTooltipOpen(true)重复触发状态更新,形成无限循环。
  2. 定时器管理失效:handleStepClick中返回的清理函数不会被React自动执行,旧定时器未被清除,多个定时器叠加触发状态更新,加剧循环问题。
  3. Tooltip显示逻辑冗余:同时使用外部propsisTooltipOpen和内部状态tooltipOpen控制显示,双重判断引发不必要的重渲染。

解决方案

1. 避免不必要的ActiveStep更新

在点击事件中,仅当点击步骤与currentStep不同时才更新activeStep,减少无效状态变更:

const handleStepClick = (index: number) => {
  if (isMobileView) {
    // 仅当点击非当前步骤时更新activeStep
    if (index !== currentStep) {
      setActiveStep(index);
    }
    // 先清除旧定时器再设置新的
    clearTimeout(tooltipTimerRef.current);
    setTooltipOpen(true);
    tooltipTimerRef.current = setTimeout(() => {
      setTooltipOpen(false);
    }, tooltipDuration);
  }
};

2. 使用Ref统一管理定时器

用useRef存储定时器ID,确保每次设置新定时器前清除旧实例,避免叠加触发:

// 组件内部添加ref存储定时器
const tooltipTimerRef = useRef<NodeJS.Timeout | null>(null);

// 修改useEffect的定时器逻辑
useEffect(() => {
  // 仅当currentStep变化时更新activeStep
  if (currentStep !== activeStep) {
    setActiveStep(currentStep);
  }
  
  if (isMobileView) {
    const isOpen = currentStep === 0 || currentStep !== -1;
    setTooltipOpen(isOpen);

    // 清除旧定时器
    if (tooltipTimerRef.current) {
      clearTimeout(tooltipTimerRef.current);
    }
    tooltipTimerRef.current = setTimeout(() => {
      setTooltipOpen(false);
    }, tooltipDuration);

    return () => {
      if (tooltipTimerRef.current) {
        clearTimeout(tooltipTimerRef.current);
      }
    };
  }
}, [currentStep, isMobileView, tooltipDuration, activeStep]);

// 修改handleStepClick的定时器管理
const handleStepClick = (index: number) => {
  if (isMobileView) {
    setActiveStep(index);
    // 清除旧定时器
    if (tooltipTimerRef.current) {
      clearTimeout(tooltipTimerRef.current);
    }
    setTooltipOpen(true);
    tooltipTimerRef.current = setTimeout(() => {
      setTooltipOpen(false);
    }, tooltipDuration);
  }
};

3. 简化Tooltip显示逻辑

如果isTooltipOpen不是必须的外部控制项,直接移除该props依赖,仅用内部状态控制:

<Tooltip
  content={item.text}
  supportingContent={item.supportingText}
  isOpen={isMobileView && activeStep === index && tooltipOpen}
  placement="top"
>

4. 优化useEffect触发条件

在useEffect中增加状态判断,仅当目标状态与当前状态不同时才执行更新,避免无效重渲染:

useEffect(() => {
  if (currentStep !== activeStep) {
    setActiveStep(currentStep);
  }
  
  if (isMobileView) {
    const isOpen = currentStep === 0 || currentStep !== -1;
    // 仅当tooltip状态不一致时更新
    if (tooltipOpen !== isOpen) {
      setTooltipOpen(isOpen);
    }

    if (tooltipTimerRef.current) {
      clearTimeout(tooltipTimerRef.current);
    }
    tooltipTimerRef.current = setTimeout(() => {
      setTooltipOpen(false);
    }, tooltipDuration);

    return () => {
      if (tooltipTimerRef.current) {
        clearTimeout(tooltipTimerRef.current);
      }
    };
  }
}, [currentStep, isMobileView, tooltipDuration, activeStep, tooltipOpen]);

内容的提问来源于stack exchange,提问作者Nicolai Bondarenco

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 11:50:54