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
相关产品推荐
相关产品推荐

