React中useEffect内声明的Socket无法在其他useEffect调用的问题
Socket.io聊天应用的状态监听问题解决
问题描述
开发Socket.io聊天应用时遇到两个矛盾问题:
- 若将socket实例声明在useEffect外部,会导致socket重复初始化
- 若将socket声明在useEffect内部,其他useEffect中调用socket会触发报错:
Uncaught TypeError: Cannot read properties of null (reading 'on')
原代码示例
const[socket,setSocket]=useState(null) const[records,setRecords]=useState([]) useEffect(()=> { axios .get('/api/messages/',{headers:{"auth-token":localStorage.getItem('jwtToken')}}) .then(res => { setRecords(res.data) console.log(records) }) .catch(err => console.log(err) ) // main socket const newsocket =io.connect(CONNECTION_PORT) newsocket.on('connect', msg => { setSocket(newsocket) console.log(`Admin ${ADMIN.id} is waiting. socketID: ${newsocket.id}`) }); newsocket.emit('waiting', ADMIN.id); return(()=>{ console.log(`admin: ${ADMIN.id} --- socketid ${newsocket.id} deleted`) newsocket.close() }); },[]); useEffect(()=>{ socket.on("socketListening", (msg) => { //Error Occurs here !!!! console.log(msg) try{ const result = records.find(e=>e.id===msg.conversationId) console.log(result) const resultMessages=result.Messages; const newMessages =[msg,...resultMessages] result.Messages=newMessages; result.lastMessage=msg.message; if(msg.senderId!==ADMIN.id){ result.Recipient=false; }else{ result.Recipient=true; } console.log(result); const arraywithoutrecord = records.filter(e=>e.id!==msg.conversationId) setRecords([result,...arraywithoutrecord]) }catch(e){ console.log(e) } }) },[records])
问题原因
- 初始渲染时
socket状态为null,第二个useEffect依赖records执行时,直接调用socket.on会触发空指针错误 - 第二个useEffect未将
socket加入依赖,即使后续socket初始化完成,也不会重新执行绑定监听
解决方案一:调整useEffect依赖与空值判断
通过给第二个useEffect添加socket依赖,增加空值判断,同时添加监听清理逻辑避免重复绑定:
const [socket, setSocket] = useState(null); const [records, setRecords] = useState([]); useEffect(() => { axios.get('/api/messages/', { headers: {"auth-token": localStorage.getItem('jwtToken')} }) .then(res => { setRecords(res.data); }) .catch(err => console.log(err)); const newsocket = io.connect(CONNECTION_PORT); newsocket.on('connect', () => { setSocket(newsocket); console.log(`Admin ${ADMIN.id} 已连接。socketID: ${newsocket.id}`); }); newsocket.emit('waiting', ADMIN.id); return () => { console.log(`admin: ${ADMIN.id} --- socketid ${newsocket.id} 已断开`); newsocket.close(); }; }, []); useEffect(() => { // 先判断socket实例是否存在,避免空指针错误 if (!socket) return; // 定义监听回调函数 const handleSocketMessage = (msg) => { console.log(msg); try { const result = records.find(e => e.id === msg.conversationId); if (!result) return; // 找不到对应会话直接返回 // 不直接修改原对象,创建新对象保证React能检测到状态变化 const updatedResult = { ...result, Messages: [msg, ...result.Messages], lastMessage: msg.message, Recipient: msg.senderId === ADMIN.id }; const arrayWithoutRecord = records.filter(e => e.id !== msg.conversationId); setRecords([updatedResult, ...arrayWithoutRecord]); } catch (e) { console.log(e); } }; // 绑定socket监听 socket.on("socketListening", handleSocketMessage); // 清理函数:移除监听,防止重复绑定和内存泄漏 return () => { socket.off("socketListening", handleSocketMessage); }; }, [socket, records]); // 依赖socket和records,状态变化时重新绑定
解决方案二:统一socket逻辑到单个useEffect(利用useRef解决闭包问题)
将所有socket相关逻辑放在同一个useEffect中,用useRef保存最新的records状态,避免闭包导致的旧状态问题:
const [socket, setSocket] = useState(null); const [records, setRecords] = useState([]); // 用ref保存最新的records,避免闭包访问旧状态 const recordsRef = useRef(records); // 同步records状态到ref useEffect(() => { recordsRef.current = records; }, [records]); useEffect(() => { axios.get('/api/messages/', { headers: {"auth-token": localStorage.getItem('jwtToken')} }) .then(res => { setRecords(res.data); recordsRef.current = res.data; }) .catch(err => console.log(err)); const newsocket = io.connect(CONNECTION_PORT); newsocket.on('connect', () => { setSocket(newsocket); console.log(`Admin ${ADMIN.id} 已连接。socketID: ${newsocket.id}`); }); newsocket.emit('waiting', ADMIN.id); // 直接在当前effect绑定监听,通过ref获取最新records const handleSocketMessage = (msg) => { console.log(msg); try { const currentRecords = recordsRef.current; const result = currentRecords.find(e => e.id === msg.conversationId); if (!result) return; const updatedResult = { ...result, Messages: [msg, ...result.Messages], lastMessage: msg.message, Recipient: msg.senderId === ADMIN.id }; const arrayWithoutRecord = currentRecords.filter(e => e.id !== msg.conversationId); setRecords([updatedResult, ...arrayWithoutRecord]); } catch (e) { console.log(e); } }; newsocket.on("socketListening", handleSocketMessage); return () => { console.log(`admin: ${ADMIN.id} --- socketid ${newsocket.id} 已断开`); newsocket.off("socketListening", handleSocketMessage); newsocket.close(); }; }, []);
关键修改点说明
- 增加socket空值判断,避免初始状态调用方法报错
- 添加监听清理函数,使用
off移除监听,防止重复绑定和内存泄漏 - 状态更新时创建新对象/数组,保证React能正确触发重渲染
- 利用useRef解决闭包中访问旧状态的问题(方案二)
内容的提问来源于stack exchange,提问作者kd12345
相关产品推荐
相关产品推荐

