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

React useEffect Hook异常排查:无依赖却多次执行问题

useEffect 异常行为排查:空依赖却多次执行、状态读取异常

问题现象

  • 未调整窗口大小,控制台却输出:
    in the useEffect Header.tsx:28:12
    in the return of useEffect Header.tsx:32:14
    in the useEffect Header.tsx:28:12
    
  • 配置空依赖数组的useEffect,按React文档说明应仅执行一次,但清理函数被触发,说明组件经历了挂载→卸载→重新挂载的过程
  • 在handleResize中执行console.log(isMobile)始终输出false,但移动端导航功能正常显示

相关代码

Header组件

import tw from "twin.macro"
import { Logo, Navbar, HamburgerMenu } from "../components";
import { useState, useEffect } from "react";

const HeaderContainer = tw.div`
  flex
  flex-row
  justify-between
  items-center
  px-4 sm:px-12 md:px-14 lg:px-16 xl:px-20 2xl:px-24
  py-6
  bg-neutral-900
  text-zinc-100
`;

const Header = () => {

  const [isMobile, setIsMobile] = useState(false);

  useEffect(() => {
    const handleResize = () => {
      setIsMobile(window.innerWidth <= 768);
    };

    // Initial check and event listener
    handleResize();
    window.addEventListener("resize", handleResize);
    console.log("in the useEffect")
    // Cleanup
    return () => {
      window.removeEventListener("resize", handleResize);
      console.log("in the return of useEffect")
    };
  }, []);

    
  return (
    <HeaderContainer>   
      <Logo />
      
      { isMobile ? <HamburgerMenu/> : <Navbar/> }
    </HeaderContainer>
  )
}

export default Header

App组件

import tw from "twin.macro"
import { Header,Hero,Footer } from "./containers";
import { BrowserRouter } from "react-router-dom";

const AppContainer = tw.div`
`;

function App() {
  return (
    <BrowserRouter>
      <AppContainer>
        <Header />
        <Hero />
        <Footer />
      </AppContainer>
    </BrowserRouter>
  )
}

export default App

原因分析

  1. 组件双重挂载的原因
    这是React 18开发环境下StrictMode的默认行为:为了提前检测组件中可能存在的副作用问题,StrictMode会强制组件执行两次挂载(挂载→卸载→重新挂载)。生产环境下该特性会自动关闭,不会出现此现象。

  2. handleResize读取旧状态的原因
    这是典型的闭包陷阱:handleResize函数在useEffect首次执行时创建,它捕获了当时的isMobile状态(初始值为false)。后续调用setIsMobile更新状态时,handleResize并不会重新创建,因此它永远只能读取到初始的false值。不过你的代码中setIsMobile是直接基于window.innerWidth判断,所以状态更新是正确的,导航功能可以正常工作,只是日志输出的是旧闭包中的值。

解决方法

1. 处理双重挂载(开发环境)

这是开发环境的正常调试机制,无需修复。如果想临时关闭该行为,找到项目入口文件(如index.tsx)中的<StrictMode>标签并移除即可,但不建议长期关闭——它能帮你提前发现潜在的副作用隐患。

2. 修复状态读取异常的问题

  • 如果只是想正确打印最新的isMobile状态,可以用useRef保存状态的最新值:
    import { useState, useEffect, useRef } from "react";
    
    const Header = () => {
      const [isMobile, setIsMobile] = useState(false);
      const isMobileRef = useRef(false);
    
      // 同步状态到ref
      useEffect(() => {
        isMobileRef.current = isMobile;
      }, [isMobile]);
    
      useEffect(() => {
        const handleResize = () => {
          const newIsMobile = window.innerWidth <= 768;
          setIsMobile(newIsMobile);
          console.log(isMobileRef.current); // 这里能拿到最新的状态值
        };
    
        handleResize();
        window.addEventListener("resize", handleResize);
        return () => window.removeEventListener("resize", handleResize);
      }, []);
    
      // ...其余代码
    }
    
  • 或者,如果需要基于旧状态计算新状态,使用setIsMobile的函数式更新:
    const handleResize = () => {
      setIsMobile(prevIsMobile => {
        const newIsMobile = window.innerWidth <= 768;
        console.log(prevIsMobile); // 这里能拿到上一次的状态值
        return newIsMobile;
      });
    };
    

内容的提问来源于stack exchange,提问作者Kaan Oflaz

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 17:23:27