React useEffect中Socket.io事件触发两次,清理订阅仍无效
React Strict Mode下Socket.io事件触发两次的解决方案
问题原因
React Strict Mode在开发环境中会刻意重复执行组件的挂载/卸载流程(即两次调用useEffect,中间执行一次清理函数),目的是检测未正确清理的副作用。你遇到的事件触发两次,大概率是因为:
- 你在组件内部创建Socket.io实例,Strict Mode导致组件两次挂载时生成了两个独立的socket连接,每个连接都绑定了事件监听,服务器发消息时两个连接都会收到通知,触发两次回调。
- 即使使用全局socket实例,若
socket.off未正确移除回调(不过你的代码中每次useEffect的回调是独立的,第一次清理时用的是绑定的同一个回调,这种情况概率较低)。
解决方案
1. 使用全局单例Socket实例(推荐)
将Socket.io实例抽离到单独模块中,确保整个应用只有一个连接:
// socket.js import { io } from 'socket.io-client'; // 替换为你的服务器地址 export const socket = io('http://your-server-domain');
在组件中导入这个单例并使用:
import { socket } from './socket'; import { useEffect } from 'react'; function ChatComponent() { useEffect(() => { const handleNewMessage = (data) => { console.log("new chat received"); // 处理消息逻辑 }; socket.on("new-chat-message", handleNewMessage); // 清理函数:移除当前回调 return () => { socket.off("new-chat-message", handleNewMessage); }; }, []); return <div>聊天组件</div>; }
这种方式下,Strict Mode的两次挂载只会绑定/解绑同一个socket实例的回调,最终只会保留一个有效监听,不会出现两次触发。
2. 用useRef确保Socket实例仅创建一次
如果必须在组件内部创建Socket实例,使用useRef保存实例,避免重复创建:
import { io } from 'socket.io-client'; import { useEffect, useRef } from 'react'; function ChatComponent() { const socketRef = useRef(null); useEffect(() => { // 仅在第一次挂载时创建socket实例 if (!socketRef.current) { socketRef.current = io('http://your-server-domain'); } const socket = socketRef.current; const handleNewMessage = (data) => { console.log("new chat received"); // 处理消息逻辑 }; socket.on("new-chat-message", handleNewMessage); return () => { socket.off("new-chat-message", handleNewMessage); }; }, []); return <div>聊天组件</div>; }
3. 临时禁用Strict Mode(仅调试用)
如果只是想快速验证问题,可以移除App.tsx中的<React.StrictMode>标签,但不推荐长期使用——Strict Mode能帮你提前发现很多潜在的副作用问题,对生产环境代码质量有帮助。
额外说明
Strict Mode的重复执行仅发生在开发环境,生产环境下不会出现该行为。但开发环境的问题建议优先通过前两种方案解决,避免隐藏其他潜在bug。
内容的提问来源于stack exchange,提问作者Aman
相关产品推荐
相关产品推荐

