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

