React (JavaScript/TypeScript SDK)

You can use the @colyseus/sdk directly in your React applications, but we also provide a separate package, @colyseus/react, that offers custom hooks for managing room connections and state subscriptions in a way that works seamlessly with React’s rendering model.

See a example project using @colyseus/react and React Three Fiber: r3f-lobby-car-prototype

Installation

npm install @colyseus/react

Hooks

useRoom(callback, deps?)

Manages the lifecycle of a Colyseus room connection. Handles connecting, disconnecting on unmount, and reconnecting when dependencies change. Works correctly with React StrictMode.

import { Client } from "@colyseus/sdk";
import { useRoom } from "@colyseus/react";
 
const client = new Client("ws://localhost:2567");
 
function Game() {
  const { room, error, isConnecting } = useRoom(
    () => client.joinOrCreate("game_room"),
  );
 
  if (isConnecting) return <p>Connecting...</p>;
  if (error) return <p>Error: {error.message}</p>;
 
  return <GameView room={room} />;
}

The first argument is a callback that returns a Promise<Room> — any Colyseus matchmaking method works (joinOrCreate, join, create, joinById, consumeSeatReservation).

Reconnecting on dependency changes:

const { room } = useRoom(
  () => client.joinOrCreate("game_room", { level }),
  [level],
);

When level changes the previous room is left and a new connection is established.

Conditional connection:

Pass a falsy value to skip connecting until a condition is met:

const { room } = useRoom(
  isReady ? () => client.joinOrCreate("game_room") : null,
  [isReady],
);

useRoomState(room, selector?)

Subscribes to Colyseus room state changes and returns immutable plain-object snapshots. Unchanged portions of the state tree keep referential equality between renders, so React components only re-render when the data they use actually changes.

import { useRoom, useRoomState } from "@colyseus/react";
 
function Game() {
  const { room } = useRoom(() => client.joinOrCreate("game_room"));
  const state = useRoomState(room);
 
  if (!state) return <p>Waiting for state...</p>;
 
  return <p>Players: {state.players.size}</p>;
}

Using a selector to subscribe to a subset of the state:

const players = useRoomState(room, (state) => state.players);

Only components that read players will re-render when the players map changes.

useLobbyRoom(callback, deps?)

This hook is designed to connect to a Colyseus Lobby Room and manage the list of available rooms. If you’re connecting to a regular game room, use useRoom() instead.

Connects to a Lobby Room and provides a live-updating list of available rooms. The list is automatically maintained as rooms are created, updated, and removed.

import { Client } from "@colyseus/sdk";
import { useLobbyRoom } from "@colyseus/react";
 
const client = new Client("ws://localhost:2567");
 
function Lobby() {
  const { rooms, error, isConnecting } = useLobbyRoom(
    () => client.joinOrCreate("lobby"),
  );
 
  if (isConnecting) return <p>Connecting...</p>;
  if (error) return <p>Error: {error.message}</p>;
 
  return (
    <ul>
      {rooms.map((room) => (
        <li key={room.roomId}>
          {room.name} — {room.clients}/{room.maxClients} players
        </li>
      ))}
    </ul>
  );
}

Return value:

FieldTypeDescription
roomsRoomAvailable<Metadata>[]Live list of available rooms
roomRoom | undefinedThe underlying lobby room connection
errorError | undefinedConnection error, if any
isConnectingbooleantrue while connecting to the lobby

useQueueRoom(connect, consume, deps?)

This hook is designed to manage the full lifecycle of a Queue Room: connecting to the queue room, tracking group size, receiving a seat reservation, confirming, and consuming the seat to join the match room. If you’re connecting to a regular game room, use useRoom() instead.

Manages the full lifecycle of a Queue Room: connecting to the queue room, tracking group size, receiving a seat reservation, confirming, and consuming the seat to join the match room. Cleans up both rooms on unmount.

import { Client } from "@colyseus/sdk";
import { useQueueRoom } from "@colyseus/react";
 
const client = new Client("ws://localhost:2567");
 
function Matchmaking() {
  const { room, clients, isWaiting, error } = useQueueRoom(
    () => client.joinOrCreate("queue", { rank: 1200 }),
    (reservation) => client.consumeSeatReservation(reservation),
  );
 
  if (error) return <p>Error: {error.message}</p>;
  if (room) return <GameScreen room={room} />;
  if (isWaiting) return <p>Waiting for match... {clients} players in group</p>;
  return <p>Connecting...</p>;
}

The first argument connects to the queue room. The second argument is called with the SeatReservation once a match is found — use client.consumeSeatReservation() to join the match room.

Return value:

FieldTypeDescription
roomRoom | undefinedThe match room, once the seat has been consumed
queueRoom | undefinedThe queue room while waiting (undefined after match is joined)
clientsnumberNumber of clients in the current matchmaking group
seatSeatReservation | undefinedThe seat reservation, once received
errorError | undefinedConnection or matchmaking error
isWaitingbooleantrue while connected to the queue and waiting for a match

Contexts

createRoomContext()

Creates a set of hooks and a RoomProvider component that share a single room connection across React reconciler boundaries (e.g. DOM + React Three Fiber). The room is stored in a closure-scoped external store rather than React Context, so the hooks work in any reconciler tree that imports them.

import { Client } from "@colyseus/sdk";
import { createRoomContext } from "@colyseus/react";
 
const client = new Client("ws://localhost:2567");
 
const { RoomProvider, useRoom, useRoomState } = createRoomContext();

Wrap your app with RoomProvider:

function App() {
  return (
    <RoomProvider connect={() => client.joinOrCreate("game_room")}>
      <UI />
      <Canvas>
        <GameScene />
      </Canvas>
    </RoomProvider>
  );
}

RoomProvider accepts a connect callback (same as the standalone useRoom hook) and an optional deps array. Pass a falsy value to connect to defer the connection.

Use the hooks in any component — DOM or R3F:

function UI() {
  const { room, error, isConnecting } = useRoom();
  const players = useRoomState((state) => state.players);
 
  if (isConnecting) return <p>Connecting...</p>;
  if (error) return <p>Error: {error.message}</p>;
 
  return <p>Players: {players?.size}</p>;
}

The returned useRoom() and useRoomState(selector?) work identically to the standalone hooks but don’t require you to pass the room as an argument.

createLobbyContext()

Creates a LobbyProvider and useLobby hook for sharing lobby room data globally across your app — useful when you need room metadata available persistently alongside an active game room, not just on a lobby screen. Like createRoomContext, it uses a closure-scoped external store so the hook works across reconciler boundaries.

import { Client } from "@colyseus/sdk";
import { createLobbyContext, createRoomContext } from "@colyseus/react";
 
const client = new Client("ws://localhost:2567");
 
const { LobbyProvider, useLobby } = createLobbyContext<MyMetadata>();
const { RoomProvider, useRoom, useRoomState } = createRoomContext();

Wrap your app with LobbyProvider (can nest with RoomProvider):

function App() {
  return (
    <LobbyProvider connect={() => client.joinOrCreate("lobby")}>
      <RoomProvider connect={() => client.joinOrCreate("game_room")}>
        <UI />
        <Canvas>
          <GameScene />
        </Canvas>
      </RoomProvider>
    </LobbyProvider>
  );
}

LobbyProvider accepts a connect callback (same as useLobbyRoom) and an optional deps array. The lobby connection persists independently of the game room.

Access lobby data from any component — even deep inside the game:

function RoomBrowser() {
  const { rooms, error, isConnecting } = useLobby();
 
  if (isConnecting) return <p>Loading rooms...</p>;
  if (error) return <p>Error: {error.message}</p>;
 
  return (
    <ul>
      {rooms.map((room) => (
        <li key={room.roomId}>
          {room.metadata.displayName} — {room.clients}/{room.maxClients}
        </li>
      ))}
    </ul>
  );
}

The returned useLobby() hook provides the same fields as useLobbyRoom (rooms, room, error, isConnecting).


Using @colyseus/sdk directly

If you prefer manual control over the room lifecycle without the @colyseus/react package, you can use @colyseus/sdk directly with useEffect:

RoomComponent.tsx
import { useEffect, useState, useRef } from "react";
import { Client, Room } from "@colyseus/sdk";
 
const client = new Client("http://localhost:2567");
 
function RoomComponent () {
    const roomRef = useRef<Room>();
    const [ players, setPlayers ] = useState([]);
 
    useEffect(() => {
        const req = client.joinOrCreate("my_room", {});
 
        req.then((room) => {
            roomRef.current = room;
            room.onStateChange((state) => setPlayers(state.players.toJSON()));
        });
 
        return () => { req.then((room) => room.leave()); };
    }, []);
 
    return (
        <div>
            {players.map((player) => (
                <div key={player.id}>{player.name}</div>
            ))}
        </div>
    );
}

The @colyseus/react hooks handle StrictMode, cleanup, and reconnection automatically. Use direct SDK access only when you need full control over the connection lifecycle.

Next Steps