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

为何useEffect清理函数在首次渲染时执行?非卸载场景解析

useEffect清理函数首次渲染后触发的原因解析

组件代码(SideMenu)

import React, { useContext, useRef } from "react";
import useMenu from "../Hooks/useMenu";
import { Context } from "../context/SideMenuContext";
import { StyledSideMenu } from "./StyledComponent/StyledSideMenu";
import Button from "./Button";
import useOnClickOutside from "../Hooks/useOnClickOutside";
import Hamburger from "./Hamburger";
function SideMenu() {
  const { IsOpen, setIsOpen } = useContext(Context);
  const wrapperref = useRef();
  const items = useMenu();
  useOnClickOutside(wrapperref, () => {
    setIsOpen(false);
  });
  return (
    <StyledSideMenu ref={wrapperref} IsOpen={IsOpen}>
      <Hamburger />
      {items}
      <Button padding="5% 10%">Resume</Button>
    </StyledSideMenu>
  );
}
export default SideMenu;

Hook代码(useOnClickOutside)

import { useEffect } from "react";
function useOnClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) {
        return;
      }
      handler();
    };
    document.addEventListener("mousedown", listener);
    document.addEventListener("touchstart", listener);
    return () => {
      document.removeEventListener("mousedown", listener);
      document.removeEventListener("touchstart", listener);
      console.log("cleaned up");
    };
  }, [ref, handler]);
}

export default useOnClickOutside;

问题

为何useEffect的清理函数会在首次渲染后执行?此时侧边菜单组件并未从DOM卸载,甚至从未卸载,仅宽度变为0。已知清理函数通常在组件卸载时执行,且应用未包裹StrictMode,需了解该行为背后的React原理。

原理解析

首先要明确React中useEffect的核心执行规则:

  • 组件挂载时,执行副作用函数;
  • 当useEffect依赖项数组中的值发生引用变化时,会先执行上一次副作用的清理函数,再执行新的副作用函数;
  • 组件卸载时,执行最后一次副作用的清理函数。

回到你的代码,清理函数触发的核心原因是依赖项的引用变化导致useEffect重新执行:

  1. 每次SideMenu组件重新渲染(比如IsOpen状态变化时,组件会因为props/state更新而重新渲染),你传给useOnClickOutside的handler参数是一个全新的箭头函数:() => {setIsOpen(false);}。因为JavaScript中每次创建的函数都是独立的引用,所以每次渲染时这个handler的引用都不一样。
  2. 第一次渲染时,useEffect执行,添加了mousedown和touchstart事件监听,同时保存了对应的清理函数。
  3. 当IsOpen变化触发组件重新渲染后,useOnClickOutside的依赖项handler发生了引用变化,React会先调用上一次副作用的清理函数(也就是你看到的console.log("cleaned up")输出),然后再执行新的副作用函数,添加新的事件监听。
  4. 至于ref参数,useRef创建的wrapperref本身的引用是固定不变的(只有ref.current会变化),所以它不是导致依赖项变化的原因。

解决办法

如果不想让清理函数在组件重新渲染时触发,可以用useCallback钩子固定handler的引用:

// 在SideMenu组件中修改handler的定义
const closeMenu = React.useCallback(() => {
  setIsOpen(false);
}, [setIsOpen]); // setIsOpen是useState返回的函数,引用固定,所以依赖项可以安全传入

useOnClickOutside(wrapperref, closeMenu);

这样closeMenu的引用会被缓存,只有当依赖项setIsOpen变化时才会重新创建(实际上setIsOpen的引用永远不会变),此时useEffect的依赖项不再变化,清理函数只会在组件卸载时执行。


内容的提问来源于stack exchange,提问作者Mohiul Islam

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 13:15:24