name: dev-multiplayer-colyseus-client description: Colyseus client SDK for React, connection methods, room events, and messaging. Use when connecting to multiplayer server. category: multiplayer
Colyseus Client SDK
Connect to Colyseus server from React applications with colyseus.js.
When to Use
Use when:
- Connecting to game server from client
- Handling room state changes
- Sending/receiving messages
- Managing room lifecycle
Basic Connection
import { Client, Room } from 'colyseus.js';
const client = new Client('ws://localhost:2567');
async function joinGame() {
try {
const room = await client.joinOrCreate('game_room', {});
room.onStateChange((state) => {
console.log('New state:', state);
});
room.onMessage('game_event', (data) => {
console.log('Received:', data);
});
room.onLeave((code) => {
console.log('Left room:', code);
});
} catch (e) {
console.error('Join error:', e);
}
}
Connection Methods
// Join existing or create new
const room = await client.joinOrCreate('room_name', { options: 'value' });
// Create new (fails if exists)
const room = await client.create('room_name', { options: 'value' });
// Join existing only (fails if full/not found)
const room = await client.join('room_name', { options: 'value' });
// Join by specific room ID
const room = await client.joinById('room_id_here', { options: 'value' });
// Reconnect to previous room
const room = await client.reconnect(reconnectionToken);
// Consume seat reservation
const room = await client.consumeSeatReservation(reservation);
React Integration
import { useEffect, useRef, useState } from 'react';
import { Client, Room } from 'colyseus.js';
const client = new Client('ws://localhost:2567');
function GameRoom() {
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);
room.onStateChange((state) => setPlayers(state.players.toJSON()));
});
return () => {
req.then((room) => room.leave());
};
}, []);
return (
<div>
{isConnecting ? 'Connecting...' :
players.map((player) => (
<div key={player.id}>{player.name}</div>
))
}
</div>
);
}
Room Methods
// Properties
room.state // Current room state (Schema instance)
room.sessionId // Unique client identifier
room.id // Room identifier (shareable)
room.name // Room handler name
// Send message to server
room.send('message_type', { data: 'value' });
// Send raw bytes
room.sendBytes(0, [0x01, 0x02, 0x03]);
// Leave room
room.leave(); // Consented leave
room.leave(false); // Forced leave
// Remove all listeners
room.removeAllListeners();
Room Events
// Message events
room.onMessage('message_type', (data) => {
console.log('Received:', data);
});
// State change events
room.onStateChange((state) => {
console.log('State updated:', state.toJSON());
});
// One-time state change (first state only)
room.onStateChange.once((state) => {
console.log('Initial state:', state);
});
// Leave event
room.onLeave((code) => {
console.log('Left room with code:', code);
// 1000 = normal shutdown
// 4000-4999 = custom codes
});
// Error event
room.onError((code, message) => {
console.error('Room error:', code, message);
});
HTTP Requests
// GET request
client.http.get('/api/endpoint').then(response => {
console.log(response.data);
});
// POST request
client.http.post('/api/endpoint', { body: 'data' }).then(response => {
console.log(response.data);
});
// PUT request
client.http.put('/api/endpoint', { body: 'data' }).then(response => {
console.log(response.data);
});
// DELETE request
client.http.delete('/api/endpoint').then(response => {
console.log(response.data);
});
React Context Provider Pattern
// RoomContext.tsx
import React, { createContext, useContext, useState, useEffect } from 'react';
import { Client, Room } from 'colyseus.js';
interface RoomContextType {
room: Room | null;
state: any;
isConnected: boolean;
join: () => void;
}
export const RoomContext = createContext<RoomContextType>({} as RoomContextType);
export function useRoom() {
return useContext(RoomContext);
}
export function RoomProvider({ children }: { children: React.ReactNode }) {
const [room, setRoom] = useState<Room | null>(null);
const [state, setState] = useState<any>(null);
const [isConnected, setIsConnected] = useState(false);
const join = async () => {
try {
const client = new Client('ws://localhost:2567');
const joinedRoom = await client.joinOrCreate('game_room');
setRoom(joinedRoom);
setIsConnected(true);
joinedRoom.onStateChange((newState) => {
setState(newState.toJSON());
});
joinedRoom.onLeave(() => {
setIsConnected(false);
setRoom(null);
});
} catch (e) {
console.error('Failed to join:', e);
}
};
return (
<RoomContext.Provider value={{ room, state, isConnected, join }}>
{children}
</RoomContext.Provider>
);
}
Authentication
// Anonymous sign-in
client.auth.signInAnonymously()
.then((response) => {
console.log('Authenticated:', response.user);
})
.catch((error) => {
console.error('Auth error:', error);
});
// Listen to auth changes
client.auth.onChange((authData) => {
if (authData.token) {
console.log('User logged in:', authData.user);
} else {
console.log('User logged out');
}
});
// Auth tokens sent automatically with HTTP requests
client.http.get('/profile').then(response => {
// Authorization header included
});
Best Practices
- Leave room on unmount - Prevent memory leaks
- Handle errors - Network failures are common
- Use reconnection tokens - For seamless reconnection
- Send inputs, not positions - Server-authoritative
- Handle state changes - Use schema callbacks for updates
Connection Lifecycle Management
CRITICAL: Handle all connection states properly for a good UX.
// Connection states enum
enum ConnectionState {
DISCONNECTED = 'disconnected',
CONNECTING = 'connecting',
CONNECTED = 'connected',
RECONNECTING = 'reconnecting',
ERROR = 'error',
}
// Connection hook with lifecycle
function useColyseusConnection(roomName: string) {
const [state, setState] = useState<ConnectionState>(ConnectionState.DISCONNECTED);
const [room, setRoom] = useState<Room | null>(null);
const [error, setError] = useState<string | null>(null);
const connect = async () => {
setState(ConnectionState.CONNECTING);
setError(null);
try {
const client = new Client('ws://localhost:2567');
const joinedRoom = await client.joinOrCreate(roomName);
setRoom(joinedRoom);
setState(ConnectionState.CONNECTED);
// Handle disconnection
joinedRoom.onLeave((code) => {
if (code === 1000) {
setState(ConnectionState.DISCONNECTED);
} else {
setState(ConnectionState.RECONNECTING);
// Attempt reconnection
setTimeout(() => connect(), 3000);
}
});
joinedRoom.onError((err) => {
setError(err.message);
setState(ConnectionState.ERROR);
});
} catch (err) {
setError(err.message);
setState(ConnectionState.ERROR);
}
};
const disconnect = () => {
if (room) {
room.leave();
setRoom(null);
setState(ConnectionState.DISCONNECTED);
}
};
return { state, error, connect, disconnect, room };
}
Reconnection Testing Pattern
For E2E tests covering disconnect/reconnect cycles:
// E2E test for reconnection
test('handles disconnect and reconnect', async ({ page }) => {
// 1. Connect to server
await page.goto('/game');
await expect(page.getByTestId('connection-status')).toHaveText('Connected');
// 2. Simulate server disconnect
await page.evaluate(() => {
// @ts-ignore - Test utility
window.__testDisconnectServer();
});
// 3. Verify disconnected state
await expect(page.getByTestId('connection-status')).toHaveText('Disconnected');
// 4. Wait for auto-reconnect
await expect(page.getByTestId('connection-status')).toHaveText('Reconnecting', { timeout: 5000 });
// 5. Verify reconnected
await expect(page.getByTestId('connection-status')).toHaveText('Connected', { timeout: 10000 });
});
Connection State Monitoring
// Browser's online/offline events
useEffect(() => {
const handleOnline = () => {
console.log('Browser online - attempt reconnect');
// Trigger reconnection logic
};
const handleOffline = () => {
console.log('Browser offline - show disconnected');
setState(ConnectionState.DISCONNECTED);
};
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
Common Mistakes
| ❌ Wrong | ✅ Right |
|---|---|
| Not leaving room on unmount | Always room.leave() in cleanup |
| Using CommonJS imports | Use import { Client } from 'colyseus.js' |
| Client sends absolute position | Client sends input (WASD, aim) |
| Ignoring error events | Always handle onError |