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/reactHooks
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:
| Field | Type | Description |
|---|---|---|
rooms | RoomAvailable<Metadata>[] | Live list of available rooms |
room | Room | undefined | The underlying lobby room connection |
error | Error | undefined | Connection error, if any |
isConnecting | boolean | true 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:
| Field | Type | Description |
|---|---|---|
room | Room | undefined | The match room, once the seat has been consumed |
queue | Room | undefined | The queue room while waiting (undefined after match is joined) |
clients | number | Number of clients in the current matchmaking group |
seat | SeatReservation | undefined | The seat reservation, once received |
error | Error | undefined | Connection or matchmaking error |
isWaiting | boolean | true 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).
Direct use of @colyseus/sdk in React
If you prefer to manage the room connection manually, you can use the @colyseus/sdk directly in your React components. Just make sure to handle the connection lifecycle properly (joining, leaving on unmount, etc.).
import { useEffect } from "react";
import { Client, Room } from "@colyseus/sdk";
const client = new Client("http://localhost:2567");
function RoomComponent () {
const roomRef = useRef<Room>();
const [ isConnecting, setIsConnecting ] = useState(true);
const [ players, setPlayers ] = useState([]);
useEffect(() => {
const req = client.joinOrCreate("my_room", {});
req.then((room) => {
roomRef.current = room;
setIsConnecting(false);
// handle room events here
room.onStateChange((state) => setPlayers(state.players.toJSON()));
});
return () => {
// make sure to leave the room when the component is unmounted
req.then((room) => room.leave());
};
}, []);
return (
<div>
{players.map((player) => (
<div key={player.id}>{player.name}</div>
))}
</div>
);
}Using a Context Provider for Room Management
Alternatively, you can use a React Context Provider to manage the connection and room state across your application.
import React, { createContext, useContext } from 'react';
import { Room } from '@colyseus/sdk';
import type { MyRoomState } from "../../backend/src/rooms/MyRoomState";
interface RoomContextType {
isConnecting: boolean;
isConnected: boolean;
room: Room;
join: () => void;
joinError: boolean;
state: any; // replace `any` with your state type
}
export const RoomContext = createContext<RoomContextType>({});
export function useRoom() { return useContext(RoomContext); }
let room!: Room;
//
// Workaround for React.StrictMode, to avoid multiple join requests
//
let hasActiveJoinRequest: boolean = false;
export function RoomProvider({ children }: { children: React.ReactNode }) {
const [searchParams, _] = useSearchParams();
const [joinError, setJoinError] = React.useState(false);
const [isConnecting, setIsConnecting] = React.useState(false);
const [isConnected, setIsConnected] = React.useState(false);
const [state, setState] = React.useState<ReturnType<MyRoomState['toJSON']>>(undefined)
const join = () => {
if (hasActiveJoinRequest) { return; }
hasActiveJoinRequest = true;
setIsConnecting(true);
try {
room = await client.joinOrCreate("my_room");
} catch (e) {
setJoinError(true);
setIsConnecting(false);
return;
} finally {
hasActiveJoinRequest = false;
}
//
// cache reconnection token, if user goes back to this URL, we can try re-connect to the room.
// TODO: do not cache reconnection token if user is spectating
//
localStorage.setItem("reconnection", JSON.stringify({
token: room.reconnectionToken,
roomId: room.roomId,
}));
room.onStateChange((state) => setState(state.toJSON()));
room.onLeave(() => setIsConnected(false));
setIsConnected(true);
};
return (
<RoomContext.Provider value={{ isConnecting, isConnected, room, join, joinError, state }}>
{children}
</RoomContext.Provider>
);
}Using a Context Provider for Authentication
You can also use a React Context Provider to manage the authentication state across your application. This is useful if you want to handle user authentication and authorization in a centralized way.
The following example shows how to create an AuthContext that provides the authentication state and automatically signs in the user anonymously if no token is available.
You can customize it to meet your specific authentication needs.
import { createContext, useContext, useState, useEffect } from 'react';
import { Client } from "@colyseus/sdk";
interface AuthContextType {
user: any;
loading: boolean;
}
const AuthContext = createContext<AuthContextType>(undefined);
export function AuthProvider({ colyseusSDK, children }: { colyseusSDK: Client, children: React.ReactNode }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const setUserData = (userData: any) => {
setUser(userData);
setLoading(false);
};
// Handle authentication on mount or token change
useEffect(() => {
colyseusSDK.auth.onChange((authData) =>
setUserData((authData.token) ? authData.user : null));
if (!colyseusSDK.auth.token) {
colyseusSDK.auth.signInAnonymously()
.then((response) => console.log("Anonymous login success:", response))
.catch((error) => console.error("Anonymous login error:", error));
}
}, [colyseusSDK]);
return (
<AuthContext.Provider value={{ user, loading, }}>
{children}
</AuthContext.Provider>
);
};
export function useAuth () {
return useContext(AuthContext);
};
React
JavaScript
Unity