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

Next.js热重载时如何避免函数重复调用防止回调重复注册

问题原因

重复订阅是两个问题叠加导致的:

  • 你在useEffect里调用subscribe后,没有执行其返回的取消订阅函数,effect重跑、组件卸载时旧订阅不会被自动移除
  • 存订阅列表的subscriptions是模块级全局变量,触发热重载(Fast Refresh/HMR)时,模块替换不会自动清空这个变量里的旧订阅,新的effect执行时又会往数组里追加新的同类型回调,最终触发多次执行。
修复方案

第一步:补全useEffect的清理逻辑

这是必须做的基础修复,覆盖生产环境正常生命周期的场景:

useEffect(() => {
  const unsubscribe = subscribe("run", () => {
    console.log("RUN!!")
  })
  // effect销毁阶段自动取消当前订阅
  return () => unsubscribe()
// 注意依赖要加subscribe,避免拿到旧引用
}, [subscribe])

第二步:稳定hook返回方法的引用

你当前实现中每次调用useEvents都会生成全新的subscribe、dispatch函数,会导致依赖这两个方法的effect不必要重跑,用useCallback包裹做引用固定:

import { useCallback } from 'react'

const subscriptions: Subscription[] = []

const useEvents = () => {
  const subscribe = useCallback((type: Type, callback: () => void) => {
    const id = nanoid(8)
    subscriptions.push({ id, type, callback })
    return () => {
      const targetIndex = subscriptions.findIndex((s) => s.id === id)
      if (targetIndex > -1) subscriptions.splice(targetIndex, 1)
    }
  }, [])

  const dispatch = useCallback((type: Type) => {
    subscriptions
      .filter((s) => s.type === type)
      .forEach(sub => sub.callback())
  }, [])

  return { subscribe, dispatch }
}

第三步:处理开发环境热重载的残留状态

热重载触发时模块不会完全重载,模块级的subscriptions会保留旧值,你可以对应你用的构建工具加HMR清理逻辑,热替换前清空旧订阅:
如果是Vite环境:

if (import.meta.env.DEV && import.meta.hot) {
  import.meta.hot.dispose(() => {
    subscriptions.length = 0
  })
}

如果是Webpack环境:

if (process.env.NODE_ENV === 'development' && module.hot) {
  module.hot.dispose(() => {
    subscriptions.length = 0
  })
}

这段逻辑只在开发环境生效,不会被打包到生产环境。

可选优化方案

如果不想写HMR相关逻辑,可以把全局订阅列表存到Context的ref里,用Provider包裹根组件:

  • 热重载时组件树重新挂载,Context里的ref会被重置
  • 所有订阅逻辑跟随组件生命周期走,配合useEffect的清理逻辑就不会有残留
    这种方案更符合React的数据流设计,适合中大型项目使用。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.02 23:48:36