点击元素触发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> ); }, ), );
错误原因分析
- 状态更新循环触发:点击当前步骤时,
handleStepClick中调用setActiveStep(index)(此时index等于currentStep),触发useEffect(依赖包含currentStep)重新执行,useEffect里又会调用setTooltipOpen(isOpen),和handleStepClick中的setTooltipOpen(true)重复触发状态更新,形成无限循环。 - 定时器管理失效:
handleStepClick中返回的清理函数不会被React自动执行,旧定时器未被清除,多个定时器叠加触发状态更新,加剧循环问题。 - Tooltip显示逻辑冗余:同时使用外部props
isTooltipOpen和内部状态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
相关产品推荐
相关产品推荐

