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.

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.

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.


Using @colyseus/sdk Directly 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.).

RoomComponent.tsx
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.

RoomContext.tsx
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.

AuthContext.tsx
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);
};