Search code examples
reactjstypescriptsocket.iofastify

Socket room event not received in React component


I'm developing a turn based game with React, Fastify and Socket.io for a scholar project.

The player A create a game which create a socket room server-side using the game id as room id. The, he wait in a "lobby" which is my Lobby.tsx component :

export function Lobby() {
    // Other stuff ...
    const {socket, emitEvent, listenEvent, dropEvent} = useContext(SocketContext)

    socket.on(PokeBattleSocketEvents.GAME_PLAYER_JOINED, (data: object) => {
        console.log("player joined", data);
        toast.custom((t) => <Toast t={t} msg={`Player ${data.player} joined your lobby !`} />)

    })

    return (<></>)

}

When a player B try to join the game, it send an socket event to the server so it can send an event to the room so player A is aware that player B has joined the game. Here's the Reac Component which send the event to the server :

export function GameRow(props) {
    const {socket, emitEvent} = useContext(SocketContext)
    const navigate = useNavigate()

    const joinGame = async () => {
        const response = await gameJoin(props.token, props.userId, props.game.id, PokeBattleGameActions.JOIN);
        if (response.error) {
            toast.custom((t) => <Toast t={t} msg={response.error} level={"danger"}/>)
        } else {
            socket.emit(PokeBattleSocketEvents.GAME_PLAYER_JOINING, {roomId: props.game.id, userId: props.userId})
            navigate("/game/lobby/" + props.game.id)
        }
    }

    return (
        <>
            <tr>
                <td>
                    {props.game.creator}
                </td>
                <td>
                    <Button label={"Join game"} btnWidth={"btn-sm"} click={joinGame} />
                </td>
            </tr>
        </>
    );
}

Player B is correctly redirected to the "lobby" after it emit the "GAME_PLAYER_JOINING" event to the server.

Server side, this event is well received but when it emit an event to the room. Here's the server side code :

app.io.on(PokeBattleSocketEvents.CONNECTION, (socket) => {
    socket.on(PokeBattleSocketEvents.GAME_CREATE_ROOM, async (data) => {
        const game = await Game.findByPk(data.gameId)
        socket.join(game.dataValues.id)
    })

    socket.on(PokeBattleSocketEvents.GAME_PLAYER_JOINING, async (data) => {
        socket.join(data.roomId)
        console.log("dummy log")
        socket.to(data.roomId).emit(PokeBattleSocketEvents.GAME_PLAYER_JOINED, {
            'player': data.userId,
            'gameId': data.roomId
        })
    })

})

But then, player A does not receive the "GAME_PLAYER_JOINED" event that is handled in the Lobby.tsx component as you saw in the first code fragment (The log and toaster are not fired).

I can confirm that the socket room id is the same for both player (using game id) and the server should emit the last event as it logged the "dummy log" message.

The socket is importer into component with useContext(SocketContext) :

export const SocketProvider = ({children}) => {
    const socket = io(import.meta.env.VITE_API_ENDPOINT);

    useEffect(() => {
        socket.on(PokeBattleSocketEvents.TEST_EVENT, (data: object) => {
            console.log(data)

            return () => socket.off(PokeBattleSocketEvents.TEST_EVENT)
        });

        socket.on(PokeBattleSocketEvents.GAME_CREATE_ROOM, (data: object) => {
            console.log(data)
        })

        return () => {
            socket.off(PokeBattleSocketEvents.TEST_EVENT)
            socket.off(PokeBattleSocketEvents.GAME_CREATE_ROOM)
        }
    }, []);

    const emitEvent = async (event: string, data: object) => {
        socket.emit(event, data)
    }

    return (
        <SocketContext.Provider value={{socket, emitEvent, listenEvent, dropEvent}} >
            {children}
            <Outlet/>
        </SocketContext.Provider>
    )
}

My problem is that player A is not receiving the socket room event.

I've checked similar question on SO but with no luck :

useEffect(() => {
        socket.on(PokeBattleSocketEvents.GAME_PLAYER_JOINED, (data: object) => {
            console.log("player joined", data);
            toast.custom((t) => <Toast t={t} msg={`Player ${data.player} joined your lobby !`} />)
            setPlayer2(data.player)
        })

        return () => {socket.off(PokeBattleSocketEvents.GAME_PLAYER_JOINED);}
    }, [socket]);

Edit

  • Updated context provider

Solution

  • In your version of the code, the socket.io instance is recreated in every render inside the SocketProvider, so after a render it resets all the room information you have.

    You can fix it by initializing the socket only once, either with a useState or a useMemo. With useState it's usually better because useMemo isn't guaranteed to run only once.

    export const SocketProvider = ({children}) => {
      const [socket] = useState(() => io(import.meta.env.VITE_API_ENDPOINT));
      // ...
    };
    

    If you need to share the same socket in multiple pages then it may make sense to define the socket outside of a component. In that case consider using the following version with useEffect.

    const socket = io(import.meta.env.VITE_API_ENDPOINT, { autoConnect: false });
    export const SocketProvider = ({children}) => {
      // ...
      useEffect(() => {
        socket.connect();
        return () => socket.disconnect();
      }, []);
      // ...
    };
    

    But be aware that in StrictMode in development the cleanup function will be called also while mounting. In that case you could opt for another hook like useOnce.