Search code examples
reactjsreact-hooksreact-reduxsocket.ioredux-toolkit

useSelector getting default state on socket.io events


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:

  1. Why in A, the state in the auth slice was logged correctly, but the default one was logged in the game slice?
  2. Why is that behavior happening in B?

I would appreciate any insights :D


Solution

  • 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.