Next.js使用useMediaQuery触发未挂载组件状态更新报错
问题描述
在Next.js项目中使用useMediaQuery实现背景图条件渲染时,控制台抛出如下报错:
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
已尝试添加useEffect清理函数但问题未解决,报错复现过程如下:
关联业务代码
export const getStaticProps: GetStaticProps = async () => { const rocketData = await fetchPlanetsInfo("technology"); return { props: { data: rocketData, }, }; }; export default function Technology({ data }: Props) { const isMobile = useMediaQuery({ query: "(max-width: 30em)" }); const [mobileView, setMobileView] = React.useState(false); React.useEffect(() => { setMobileView(isMobile); }, [isMobile]); return ( <Layout> <MainPagesComponents backgroundImage={spaceLaunchBackground} title={"03 SPACE LAUNCH 101"} > <Swiper // direction={"vertical"} modules={[Pagination, Autoplay]} spaceBetween={0} slidesPerView={"auto"} centeredSlides={true} scrollbar={{ draggable: true }} autoplay={{ delay: 5000, pauseOnMouseEnter: true, disableOnInteraction: false, }} pagination={{ clickable: true, bulletActiveClass: "tech-active-class", bulletClass: "swiper-tech", horizontalClass: "swiper-tech-position-container", renderBullet: (index, className) => { return ` <div class='${className}'>${index + 1}</div> `; }, }} className="mySwiper" > {data.map((rocket) => { const portraitImage = rocket.images.portrait.slice(1); const landscapeImage = rocket.images.landscape.slice(1); const viewImage = { width: mobileView ? "375px" : "515px", height: mobileView ? "170px" : "527px", }; return ( <SwiperSlide key={uuidv4()}> <TechnologySlider view={viewImage} image={mobileView ? landscapeImage : portraitImage} title={rocket.name.toUpperCase()} description={rocket.description} /> </SwiperSlide> ); })} </Swiper> </MainPagesComponents> </Layout> ); }
问题原因分析
- 认知误区澄清:
getStaticProps仅在服务端构建/预渲染阶段执行,不会在客户端运行,该报错和getStaticProps本身逻辑无关,实际是页面hydration、客户端路由切换过程中,组件卸载后触发了状态更新导致的。 - 冗余状态同步问题:额外声明的
mobileView状态完全多余,useMediaQuery本身就会返回响应式的最新匹配结果,套一层useEffect做状态同步拉长了状态更新链路,很容易在组件卸载节点触发更新。 - SSR hydration不匹配问题:服务端渲染阶段没有window对象,
useMediaQuery无法获取媒体查询匹配结果,如果没有配置默认值,hydration前后返回值会突然跳变,触发意外的更新。 - 列表key错误导致组件反复重建:在
data.map里用uuidv4()作为SwiperSlide的key,每次组件重渲染都会生成全新的key,导致Swiper实例不断销毁、重建,其内部的异步autoplay回调、事件监听很容易在实例销毁后还尝试更新状态,触发内存泄漏报错。 - 部分版本的
useMediaQuery实现存在缺陷:比如旧版react-use、自定义实现的useMediaQuery没有在cleanup函数里正确移除媒体查询的change监听,也会导致组件卸载后监听回调还在触发状态更新。
修复方案
按优先级执行以下调整即可解决问题:
- 删除冗余的
mobileView中间状态,直接使用useMediaQuery的返回值做逻辑判断,去掉无意义的状态同步逻辑。 - 给
useMediaQuery配置SSR场景下的默认值(和服务端初始渲染状态对齐,一般传false即可),避免hydration阶段值跳变。 - 移除列表渲染里动态生成
uuidv4()的写法,用数据本身的唯一标识(比如火箭的name、id字段)作为SwiperSlide的key,避免Swiper实例不必要的销毁重建。 - 如果使用的是第三方库提供的
useMediaQuery,升级到最新稳定版;如果是自定义实现的hook,确保在useEffect的清理函数中调用removeEventListener移除媒体查询的监听回调。
修复后的核心代码如下:
export default function Technology({ data }: Props) { // 配置SSR默认值,和服务端初始渲染状态对齐 const isMobile = useMediaQuery({ query: "(max-width: 30em)" }, { defaultValue: false }); return ( <Layout> <MainPagesComponents backgroundImage={spaceLaunchBackground} title={"03 SPACE LAUNCH 101"} > <Swiper modules={[Pagination, Autoplay]} spaceBetween={0} slidesPerView={"auto"} centeredSlides={true} scrollbar={{ draggable: true }} autoplay={{ delay: 5000, pauseOnMouseEnter: true, disableOnInteraction: false, }} pagination={{ clickable: true, bulletActiveClass: "tech-active-class", bulletClass: "swiper-tech", horizontalClass: "swiper-tech-position-container", renderBullet: (index, className) => { return `<div class='${className}'>${index + 1}</div>`; }, }} className="mySwiper" > {data.map((rocket) => { const portraitImage = rocket.images.portrait.slice(1); const landscapeImage = rocket.images.landscape.slice(1); const viewImage = { width: isMobile ? "375px" : "515px", height: isMobile ? "170px" : "527px", }; return ( // 用数据自带唯一字段作为key,禁止每次渲染生成新uuid <SwiperSlide key={rocket.name}> <TechnologySlider view={viewImage} image={isMobile ? landscapeImage : portraitImage} title={rocket.name.toUpperCase()} description={rocket.description} /> </SwiperSlide> ); })} </Swiper> </MainPagesComponents> </Layout> ); }
内容的提问来源于stack exchange,提问作者Drew94
相关产品推荐
相关产品推荐

