I am creating an online game using socket.io and Reactjs. I am using redux toolkit to store state. There, I have 2 slices: one for auth, and one for in-game data (e.g. roomId, socketId). Flipping cards is the main game mechanic. To listen to socket.io events, I am using an useEffect function. Here is the simplfied code:
const stateRoom = useSelector((state) => state.game.status.currentRoom);
const stateSocketId = useSelector((state) => state.game.status.socketId);
const stateUsername = useSelector((state) => state.auth.user.username);
console.log(
" Re-render: ", "stateRoom: ",stateRoom, "stateSocketId: ",stateSocketId, "stateUsername: ",stateUsername
);
const cardFlippedHandler = (data) => {
console.log(
" Data: ", "stateRoom: ",stateRoom, "stateSocketId: ",stateSocketId, "stateUsername: ",stateUsername
);
useEffect(() => {
socket.on("card_flipped", cardFlippedHandler);
return () => {
socket.off("card_flipped", cardFlippedHandler);
};
}, [socket]);
When clicking on a card, an event is emitted to the server, with the room id, then the server emit the card_flipped event to the clients in the room. Code on server:
io.on("connection", (socket) => {
socket.on("flip_card", (data) => {
io.to(data.room).emit("card_flipped", data);
});
}
So initially, when the cards load, I am getting this log:
Re-render: stateRoom: myroom stateSocketId: -_a2DG64ljS3Kl-VAAAf stateUsername: John
A. But when I click on one of the cards, I only get:
stateRoom: <empty string> stateSocketId: <empty string> stateUsername: John
I tried changing the default state of the room in the slice to be the string "default", and that is what I got in the log (stateRoom: default).
B. But THEN, I created a button, that onClick, calls the same function called on event, which is cardFlippedHandler. This is what I got:
Data: stateRoom: myroom stateSocketId: -_a2DG64ljS3Kl-VAAAf stateUsername: John
So there is:
I would appreciate any insights :D
This looks like a classic example of a "stale closure" problem.
The useEffect
and socket.on()
calls only run once, on the first render. That means that the copy of cardFlippedHandler
that they refer to is also the one that was created during that first render... before the socket connection has actually been established. So, inside of cardFlippedHandler
, it's still pointing to a stateSocketId
variable that is empty.
There's a few ways to rearrange the logic to address this, but that's the core issue.