Socket.io 连接完好,但从不卸载的 React 组件在页面导航后变得对套接字事件无响应
Socket.io connection intact but React component that never unmounts becomes unresponsive to socket events after page navigation
设置:
- Socket.io 在 React 中与 react-router-dom
- 通过 useContext 传递给子组件的 Socket 实例
- 使用 Express 构建的后端
问题:
Navbar 侦听套接字事件以更新未读消息的计数器。一切正常,直到用户导航到不同的页面。一旦发生导航,即使连接完好无损,导航栏中的套接字(永远不会卸载)也不会响应服务器发出的未来事件。
预期行为:
导航栏套接字在页面导航后仍然响应服务器发出的信号。
观察:
即使导航栏套接字没有响应,应用程序中使用套接字的其他区域仍然可以正常工作。刷新页面时导航栏套接字再次响应,但在页面导航后重复出现相同的问题。导航栏是唯一在发生导航时不会卸载的组件,而安装在导航上的组件是通过 useContext 使用套接字实例加载的。
据我所知,连接永远不会中断(断开连接事件永远不会在服务器端触发)并且新安装的组件可以发出事件,服务器响应,并向新组件所在的客户端发出响应响应而导航栏没有。
其他说明:
这是我问过的第一个问题,如果问题格式不正确,我提前道歉。代码显然已简化,以省略似乎不会影响问题的区域。服务器端代码被省略,因为它似乎接收和发出事件没有任何问题。
//App.js
const App = () => {
const socket = useRef(io('path'));
return (
<SocketContext.Provider value={socket.current}>
<BrowserRouter>
<Nav />
<Switch>
<Route path='/messages' component={Messages} />
<Route path='/' component={Home} />
<!-- Other routes -->
</Switch>
</BrowserRouter>
</SocketContext.Provider>
)
}
//Nav.js
const Nav = () => {
const socket = useContext(SocketContext);
const [newMsgCount, setNewMsgCount] = useState(0);
useEffect(() => {
socket.emit('get new msg count');
socket.on('new msg count', (count) => setNewMsgCount(count));
socket.on('new msg received', () => setNewMsgCount(prev => prev + 1));
socket.on('marked as read', () => setNewMsgCount(prev => prev - 1));
return () => {
socket.off('new msg count');
socket.off('new msg received');
socket.off('marked as read');
}
}, [socket]);
return (
//Omitted for brevity
)
}
//Messages.js
const Messages = () => {
const socket = useContext(SocketContext);
const [msgs, setMsgs] = useState([]);
const [newMsg, setNewMsg] = useState({ to: '', body: '' });
useEffect(() => {
socket.emit('get all msgs');
socket.on('all msgs', (data) => setMsgs(data));
socket.on('new msg received', (data) => setMsgs(prev => ([...prev, data])));
return () => {
socket.off('all msgs');
socket.off('received new msg');
}
}, [socket]);
const send = () => socket.emit('new msg', newMsg);
return (
<div>
<!-- Omitted for brevity -->
<form onSubmit={send}>
<!-- Omitted for brevity -->
<button>Send</button>
</form>
</div>
)
}
由于您的套接字希望从第一个页面加载开始直到您关闭页面(我假设是这样......),我建议将套接字初始化与 React
分离。将它放在 ref
中似乎是不对的,因为即使应用程序组件仍然有机会卸载并再次安装(正如您在页面加载时所经历的那样)或对其产生其他副作用。
例如有一个文件socket.js
import React from "react";
import io from "socket.io-client";
// have a standalone variable holding the socket.
const path = '...'
export const socket = io(path);
export const SocketContext = React.createContext(socket);
然后在你的 APP.js
中这样做:
import React from "react";
import SocketContext, { socket } from "./socket";
const App = () => (
<SocketContext.Provider value={socket}>
<OtherComponents />
</SocketContext.Provider>
);
或在任何其他文件中使用它my-outsourced-mapping.js
import { socket } from "./socket";
// say we have redux
import store from './store'
export const veryLargeMappingFunctionForThisParticularShittyEventMyBackendGuyDid() {
socket.on('complex-data-event', data => {
mapped = // reduce, map, group and more
store.dispatch('simple-data-event', mapped)
})
}
您可以调整它以在登录成功后只初始化套接字..
我在很多项目中都遇到过这个问题,我从来没有遇到过这个解决方案这样令人头疼的问题 - 保持简单。
问题
您 disconnecting/unsubscribing 插座不正确。 socket.off
有两个参数,第二个是你想要从事件中 disconnect/unlisten/unsubscribe 的侦听器回调。
socket.off(eventName, listener)
Removes the specified listener from the listener array for the event
named eventName.
const listener = (...args) => {
console.log(args);
}
socket.on("details", listener);
// and then later...
socket.off("details", listener);
解决方案
将侦听器重构为可以传递给 socket.on
和 socket.off
的独立函数。
Nav.js
const Nav = () => {
const socket = useContext(SocketContext);
const [newMsgCount, setNewMsgCount] = useState(0);
useEffect(() => {
socket.emit('get new msg count');
const incrementCount = () => setNewMsgCount(prev => prev + 1);
const decrementCount = () => setNewMsgCount(prev => prev - 1);
socket.on('new msg count', setNewMsgCount);
socket.on('new msg received', incrementCount);
socket.on('marked as read', decrementCount);
return () => {
socket.off('new msg count', setNewMsgCount);
socket.off('new msg received', incrementCount);
socket.off('marked as read', decrementCount);
}
}, [socket]);
...
}
Messages.js
const Messages = () => {
const socket = useContext(SocketContext);
const [msgs, setMsgs] = useState([]);
const [newMsg, setNewMsg] = useState({ to: '', body: '' });
useEffect(() => {
socket.emit('get all msgs');
const messageRecdHandler = (data) => setMsgs(prev => ([...prev, data]));
socket.on('all msgs', setMsgs);
socket.on('new msg received', messageRecdHandler);
return () => {
socket.off('all msgs', setMsgs);
socket.off('received new msg', messageRecdHandler);
}
}, [socket]);
const send = () => socket.emit('new msg', newMsg);
...
}
我同意 Silven 的观点,套接字可能应该从父 React 组件代码中移出 。实例化一次并设置为单例的上下文值。
设置:
- Socket.io 在 React 中与 react-router-dom
- 通过 useContext 传递给子组件的 Socket 实例
- 使用 Express 构建的后端
问题:
Navbar 侦听套接字事件以更新未读消息的计数器。一切正常,直到用户导航到不同的页面。一旦发生导航,即使连接完好无损,导航栏中的套接字(永远不会卸载)也不会响应服务器发出的未来事件。
预期行为:
导航栏套接字在页面导航后仍然响应服务器发出的信号。
观察:
即使导航栏套接字没有响应,应用程序中使用套接字的其他区域仍然可以正常工作。刷新页面时导航栏套接字再次响应,但在页面导航后重复出现相同的问题。导航栏是唯一在发生导航时不会卸载的组件,而安装在导航上的组件是通过 useContext 使用套接字实例加载的。
据我所知,连接永远不会中断(断开连接事件永远不会在服务器端触发)并且新安装的组件可以发出事件,服务器响应,并向新组件所在的客户端发出响应响应而导航栏没有。
其他说明:
这是我问过的第一个问题,如果问题格式不正确,我提前道歉。代码显然已简化,以省略似乎不会影响问题的区域。服务器端代码被省略,因为它似乎接收和发出事件没有任何问题。
//App.js
const App = () => {
const socket = useRef(io('path'));
return (
<SocketContext.Provider value={socket.current}>
<BrowserRouter>
<Nav />
<Switch>
<Route path='/messages' component={Messages} />
<Route path='/' component={Home} />
<!-- Other routes -->
</Switch>
</BrowserRouter>
</SocketContext.Provider>
)
}
//Nav.js
const Nav = () => {
const socket = useContext(SocketContext);
const [newMsgCount, setNewMsgCount] = useState(0);
useEffect(() => {
socket.emit('get new msg count');
socket.on('new msg count', (count) => setNewMsgCount(count));
socket.on('new msg received', () => setNewMsgCount(prev => prev + 1));
socket.on('marked as read', () => setNewMsgCount(prev => prev - 1));
return () => {
socket.off('new msg count');
socket.off('new msg received');
socket.off('marked as read');
}
}, [socket]);
return (
//Omitted for brevity
)
}
//Messages.js
const Messages = () => {
const socket = useContext(SocketContext);
const [msgs, setMsgs] = useState([]);
const [newMsg, setNewMsg] = useState({ to: '', body: '' });
useEffect(() => {
socket.emit('get all msgs');
socket.on('all msgs', (data) => setMsgs(data));
socket.on('new msg received', (data) => setMsgs(prev => ([...prev, data])));
return () => {
socket.off('all msgs');
socket.off('received new msg');
}
}, [socket]);
const send = () => socket.emit('new msg', newMsg);
return (
<div>
<!-- Omitted for brevity -->
<form onSubmit={send}>
<!-- Omitted for brevity -->
<button>Send</button>
</form>
</div>
)
}
由于您的套接字希望从第一个页面加载开始直到您关闭页面(我假设是这样......),我建议将套接字初始化与 React
分离。将它放在 ref
中似乎是不对的,因为即使应用程序组件仍然有机会卸载并再次安装(正如您在页面加载时所经历的那样)或对其产生其他副作用。
例如有一个文件socket.js
import React from "react";
import io from "socket.io-client";
// have a standalone variable holding the socket.
const path = '...'
export const socket = io(path);
export const SocketContext = React.createContext(socket);
然后在你的 APP.js
中这样做:
import React from "react";
import SocketContext, { socket } from "./socket";
const App = () => (
<SocketContext.Provider value={socket}>
<OtherComponents />
</SocketContext.Provider>
);
或在任何其他文件中使用它my-outsourced-mapping.js
import { socket } from "./socket";
// say we have redux
import store from './store'
export const veryLargeMappingFunctionForThisParticularShittyEventMyBackendGuyDid() {
socket.on('complex-data-event', data => {
mapped = // reduce, map, group and more
store.dispatch('simple-data-event', mapped)
})
}
您可以调整它以在登录成功后只初始化套接字..
我在很多项目中都遇到过这个问题,我从来没有遇到过这个解决方案这样令人头疼的问题 - 保持简单。
问题
您 disconnecting/unsubscribing 插座不正确。 socket.off
有两个参数,第二个是你想要从事件中 disconnect/unlisten/unsubscribe 的侦听器回调。
socket.off(eventName, listener)
Removes the specified listener from the listener array for the event named eventName.
const listener = (...args) => { console.log(args); } socket.on("details", listener); // and then later... socket.off("details", listener);
解决方案
将侦听器重构为可以传递给 socket.on
和 socket.off
的独立函数。
Nav.js
const Nav = () => {
const socket = useContext(SocketContext);
const [newMsgCount, setNewMsgCount] = useState(0);
useEffect(() => {
socket.emit('get new msg count');
const incrementCount = () => setNewMsgCount(prev => prev + 1);
const decrementCount = () => setNewMsgCount(prev => prev - 1);
socket.on('new msg count', setNewMsgCount);
socket.on('new msg received', incrementCount);
socket.on('marked as read', decrementCount);
return () => {
socket.off('new msg count', setNewMsgCount);
socket.off('new msg received', incrementCount);
socket.off('marked as read', decrementCount);
}
}, [socket]);
...
}
Messages.js
const Messages = () => {
const socket = useContext(SocketContext);
const [msgs, setMsgs] = useState([]);
const [newMsg, setNewMsg] = useState({ to: '', body: '' });
useEffect(() => {
socket.emit('get all msgs');
const messageRecdHandler = (data) => setMsgs(prev => ([...prev, data]));
socket.on('all msgs', setMsgs);
socket.on('new msg received', messageRecdHandler);
return () => {
socket.off('all msgs', setMsgs);
socket.off('received new msg', messageRecdHandler);
}
}, [socket]);
const send = () => socket.emit('new msg', newMsg);
...
}
我同意 Silven 的观点,套接字可能应该从父 React 组件代码中移出 。实例化一次并设置为单例的上下文值。