React useContext导致页面加载时重置WebSocket连接问题求助
解决Next.js中WebSocket页面刷新后连接重置、退出房间的问题
页面刷新时客户端应用会完全重启,原有Socket连接断开,同时客户端Socket实例被重新创建,导致丢失房间状态。以下是具体解决方案:
一、确保客户端Socket实例单例化
当前直接在模块顶层创建Socket实例,页面刷新时模块重新加载会生成新连接,改成单例模式避免重复创建:
修改/contexts/socket.tsx:
import socketio from "socket.io-client"; import React from "react"; // 单例存储Socket实例 let socketInstance: ReturnType<typeof socketio> | null = null; export const getSocketInstance = () => { if (!socketInstance) { socketInstance = socketio({ // 开启自动重连配置 reconnection: true, reconnectionDelay: 1000, reconnectionDelayMax: 5000, reconnectionAttempts: Infinity }); } return socketInstance; }; export const SocketContext = React.createContext(getSocketInstance());
更新pages/_app.tsx使用单例:
import { SocketContext, getSocketInstance } from '../contexts/socket'; const App = ({ Component, pageProps }: AppProps) => { const socket = getSocketInstance(); return ( <SocketContext.Provider value={socket}> <main> <Component {...pageProps} key={router.asPath} /> <Toaster position="bottom-center" toastOptions={{ duration: 3000, }} /> </main> </SocketContext.Provider> ) }; export default wrapper.withRedux(App);
二、页面刷新后自动恢复房间状态
将用户当前所在房间ID存入本地存储,页面加载后自动触发加入房间操作:
修改pages/chat/index.tsx:
import { useState, useContext, useEffect } from "react"; import AppLayout from "../../layouts/AppLayout"; import useUser from '../../hooks/useUser'; import { SocketContext } from '../../contexts/socket'; // 定义Message类型 type Message = { author: string; message: string; }; export default function Chat() { const username = useUser().username; const [messages, setMessages] = useState<Array<Message>>([]); const socket = useContext(SocketContext); // 实际项目中替换为真实房间ID(比如从路由参数获取) const roomId = 'target-room-id'; useEffect(() => { // 页面加载时读取本地存储的房间ID,自动加入 const savedRoom = localStorage.getItem('current-chat-room'); if (savedRoom) { socket.emit('joinRoom', savedRoom); } // 订阅消息事件,使用命名函数方便后续取消订阅 const handleNewMessage = (msg: any) => { setMessages(prev => [...prev, { author: msg.author, message: msg.message }]); }; socket.on("newIncomingMessage", handleNewMessage); // 加入目标房间并保存到本地存储 socket.emit('joinRoom', roomId); localStorage.setItem('current-chat-room', roomId); // 组件卸载时取消事件订阅,避免内存泄漏 return () => { socket.off("newIncomingMessage", handleNewMessage); }; }, [socket, roomId]) return ( <AppLayout> {/* 聊天界面渲染逻辑 */} </AppLayout> ) }
三、服务器端配合处理重连逻辑
在服务器端维护用户与房间的映射,处理客户端重连时自动恢复房间状态:
修改pages/api/socket.tsx:
import { Server } from "socket.io"; // 存储socket.id与房间ID的映射,生产环境可替换为Redis const socketRoomMap = new Map<string, string>(); export default function SocketHandler(req: any, res: any) { if (res.socket.server.io) { console.log("server socket already set up"); res.end(); return; } const io = new Server(res.socket.server, { cors: { origin: process.env.NEXT_PUBLIC_APP_URL || "*" } }); res.socket.server.io = io; const onConnection = (socket: any) => { console.log('server new socket.id: ', socket.id); // 重连时自动加入之前的房间 const prevRoom = socketRoomMap.get(socket.id); if (prevRoom) { socket.join(prevRoom); console.log(`Socket ${socket.id} reconnected to room ${prevRoom}`); } messageHandler(io, socket); // 断开连接时暂存房间信息(5分钟后清除,避免内存占用) socket.on('disconnect', () => { const room = socketRoomMap.get(socket.id); if (room) { setTimeout(() => socketRoomMap.delete(socket.id), 5 * 60 * 1000); } console.log(`Socket ${socket.id} disconnected`); }); }; io.on("connection", onConnection); res.end(); } function messageHandler(io: any, socket: any) { const createdMessage = (msg: any) => { socket.broadcast.emit("newIncomingMessage", msg); }; // 处理加入房间事件,保存映射关系 const joinRoom = (roomId: string) => { socket.join(roomId); socketRoomMap.set(socket.id, roomId); console.log(`Socket ${socket.id} joined room ${roomId}`); }; socket.on("createdMessage", createdMessage); socket.on("joinRoom", joinRoom); };
核心优化点总结
- 单例模式保证客户端Socket实例唯一,避免页面刷新时创建新连接
- 本地存储+服务器端映射,实现页面刷新后自动恢复房间状态
- 组件内管理事件订阅的生命周期,避免内存泄漏
内容的提问来源于stack exchange,提问作者rockets4all
相关产品推荐
相关产品推荐

