Next.js页面切换时如何清除setTimeout避免运行时错误
解决Next.js轮播组件页面切换后setTimeout引发的Runtime Error
我在Next.js项目中开发了一个轮播组件,用户点击链接切换页面后出现Unhandled Runtime Error。排查后发现是因为组件卸载后,之前的setTimeout仍会执行,尝试操作新页面中不存在的DOM元素。请问如何在用户切换页面后清除这些setTimeout?
错误截图:
我的组件代码:
import { useEffect, useState } from "react"; import SlideContent from "./slide-content"; import SlideDots from "./slide-dots"; import SlideItem from "./slide-item"; const Slide = (props) => { const { slides } = props; const [slideLength, setSlideLength] = useState(slides ? slides.length : 0); const [slideCounter, setSlideCounter] = useState(1); const handleSlideShow = () => { if (slideCounter < slideLength) { document.querySelector( `.slide-content:nth-of-type(${slideCounter})` ).style.left = "100%"; const setSlide = slideCounter + 1; setSlideCounter(setSlide); setTimeout(() => { document.querySelector( `.slide-content:nth-of-type(${setSlide})` ).style.left = 0; }, 250); } else { document.querySelector( `.slide-content:nth-of-type(${slideCounter})` ).style.left = "100%"; setSlideCounter(1); setTimeout(() => { document.querySelector(`.slide-content:nth-of-type(1)`).style.left = 0; }, 250); } }; useEffect(() => { if (slideLength > 0) { setTimeout(() => { handleSlideShow(); }, 5000); } }, [slideCounter, setSlideCounter]); return ( <> <div className="slide-button-arrow slide-next"> <span className="carousel-control-prev-icon"></span> </div> <div className="slide"> {slides.map((slide) => ( <SlideContent key={`slide-${slide.id}`}> <SlideItem img={slide.img} title={slide.title} /> </SlideContent> ))} <SlideDots activeDot={slideCounter} totalDots={slides} /> </div> <div className="slide-button-arrow slide-prev"> <span className="carousel-control-next-icon"></span> </div> </> ); }; export default Slide;
该轮播组件被用在首页文件中。
核心解决思路
在React组件卸载时清除所有未执行的setTimeout,同时跟踪组件内创建的所有定时器ID,避免内存泄漏和DOM操作报错。
具体修改步骤
- 添加定时器ID存储:用
useRef保存所有定时器ID,它的值在组件生命周期内持久化,不会随渲染重置。 - 更新定时器创建逻辑:每次调用
setTimeout时,将返回的ID存入useRef数组。 - 组件卸载时清理定时器:在
useEffect的清理函数中,遍历所有定时器ID并调用clearTimeout。 - 添加DOM存在性校验:操作DOM前先判断元素是否存在,避免即使定时器未被清除也不会报错。
修改后的代码:
import { useEffect, useState, useRef } from "react"; import SlideContent from "./slide-content"; import SlideDots from "./slide-dots"; import SlideItem from "./slide-item"; const Slide = (props) => { const { slides } = props; const [slideLength, setSlideLength] = useState(slides ? slides.length : 0); const [slideCounter, setSlideCounter] = useState(1); // 存储所有定时器ID const timerRef = useRef([]); const handleSlideShow = () => { if (slideCounter < slideLength) { const currentSlide = document.querySelector(`.slide-content:nth-of-type(${slideCounter})`); if (currentSlide) currentSlide.style.left = "100%"; const setSlide = slideCounter + 1; setSlideCounter(setSlide); // 保存定时器ID const timerId = setTimeout(() => { const targetSlide = document.querySelector(`.slide-content:nth-of-type(${setSlide})`); if (targetSlide) targetSlide.style.left = 0; }, 250); timerRef.current.push(timerId); } else { const currentSlide = document.querySelector(`.slide-content:nth-of-type(${slideCounter})`); if (currentSlide) currentSlide.style.left = "100%"; setSlideCounter(1); // 保存定时器ID const timerId = setTimeout(() => { const firstSlide = document.querySelector(`.slide-content:nth-of-type(1)`); if (firstSlide) firstSlide.style.left = 0; }, 250); timerRef.current.push(timerId); } }; useEffect(() => { if (slideLength > 0) { // 保存定时器ID const timerId = setTimeout(() => { handleSlideShow(); }, 5000); timerRef.current.push(timerId); } // 组件卸载时清除所有定时器 return () => { timerRef.current.forEach(timerId => clearTimeout(timerId)); }; }, [slideCounter, slideLength]); return ( <> <div className="slide-button-arrow slide-next"> <span className="carousel-control-prev-icon"></span> </div> <div className="slide"> {slides.map((slide) => ( <SlideContent key={`slide-${slide.id}`}> <SlideItem img={slide.img} title={slide.title} /> </SlideContent> ))} <SlideDots activeDot={slideCounter} totalDots={slides} /> </div> <div className="slide-button-arrow slide-prev"> <span className="carousel-control-next-icon"></span> </div> </> ); }; export default Slide;
额外优化建议
彻底避免直接操作DOM,改用React状态控制样式:
- 给
SlideContent组件传递激活状态:
// SlideContent组件示例 const SlideContent = ({ children, isActive }) => { return ( <div className={`slide-content ${isActive ? 'active' : ''}`}> {children} </div> ); };
- 对应的CSS:
.slide-content { position: absolute; left: 100%; transition: left 0.25s ease; } .slide-content.active { left: 0; }
- 在
Slide组件中通过slideCounter判断激活状态:
{slides.map((slide, index) => ( <SlideContent key={`slide-${slide.id}`} isActive={slideCounter === index + 1} > <SlideItem img={slide.img} title={slide.title} /> </SlideContent> ))}
这种方式完全遵循React数据流逻辑,从根源消除DOM操作引发的报错风险。
内容的提问来源于stack exchange,提问作者Amir Hossein
相关产品推荐
相关产品推荐

