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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.01 02:18:35