@colyseus/react

中文文档
0.18.4发布于 2026/10/03TypeScriptESM + CJS零依赖近 30 天下载 2731

React hooks for Colyseus multiplayer applications.

原始使用文档
文档目录22 节
  1. Installation
  2. Server-side rendering
  3. Hooks
  4. useRoom(callback, deps?)
  5. useRoomState(room, selector?)
  6. useRoomMessage(room, type, callback)
  7. useLobbyRoom(callback, deps?)
  8. useQueueRoom(connect, consume, deps?)
  9. Netcode hooks (Colyseus 0.18+)
  10. usePredict(room, options?)
  11. useInput(room, options?)
  12. usePredictLoop(room, onSteps, options?)
  13. useReconciler(room, select, options)
  14. useAttachAll(room, key, config, deps?)
  15. useEventChannel(room, options)
  16. useEntityInstance(room, select) / useSessionEntity(room, co…
  17. getSchemaInstance(snapshot)
  18. useInputBuffer()
  19. Contexts
  20. createRoomContext()
  21. createLobbyContext()
  22. Credits

@colyseus/react

React hooks for Colyseus multiplayer applications.

Installation

npm install @colyseus/react

Peer dependencies: @colyseus/sdk, @colyseus/schema, and react (>=18.3.1).

Server-side rendering

Every hook is safe to render on the server. A room is a live client-side connection and is never serialized for hydration, so on the server the hooks render as though nothing is connected — useRoomState() returns undefined, useRoom() reports isConnecting: true, and the predict hooks return undefined. Connection happens in an effect, so the live room arrives on the re-render right after hydration.

Render the connecting state rather than assuming state exists:

const players = useRoomState((state) => state.players);
if (!players) return <Spinner />;   // server render + first client paint

The package ships a "use client" directive, so you can import it from a Next.js App Router Server Component without the build failing. The hooks themselves still run on the client, as any React hook does.

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.

useRoomMessage(room, type, callback)

Subscribes to Colyseus room messages. The callback is kept in a ref so it is always up-to-date without re-subscribing. Automatically unsubscribes when the room changes or the component unmounts.

import { useRoom, useRoomMessage } from "@colyseus/react";

function Chat() {
  const { room } = useRoom(() => client.joinOrCreate("game_room"));
  const [messages, setMessages] = useState<string[]>([]);

  useRoomMessage(room, "chat", (message) => {
    setMessages((prev) => [...prev, message]);
  });

  return (
    <ul>
      {messages.map((msg, i) => <li key={i}>{msg}</li>)}
    </ul>
  );
}

Pass "*" as the type to listen to all message types.

useLobbyRoom(callback, deps?)

Connects to a Colyseus 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?)

Manages the full lifecycle of a Colyseus matchmaking queue: 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

Netcode hooks (Colyseus 0.18+)

React bindings for the @colyseus/sdk Predict tools — client prediction, reconciliation and remote smoothing. See the netcode docs for the underlying concepts.

The one rule: structure renders through snapshots, motion renders through predict reads. useRoomState re-renders at patch rate with raw synced values — rendering positions from it gives you jittery patch-rate motion. Predicted/smoothed values change every frame and must stay out of the React render cycle: read them with predict.value(instance, field) inside your frame loop (R3F useFrame, or the usePredictLoop callback) and write into refs.

All hooks below exist in two forms: standalone (useX(room, ...)) and bound (returned by createRoomContext(options), no room argument). Pass Predict options once via createRoomContext({ predict: { mode: "lerp", delay: 100 } }).

usePredict(room, options?)

Returns the room's shared Predict instance. Predict.get() constructs a fresh instance per call, so the hook memoizes one per room, reference-counts consumers, and disposes it when the last consumer unmounts.

useInput(room, options?)

Returns the room's InputHandle (room.input() — idempotent, stable per room). Stage fields on input.data, then input.send() once per fixed step inside the usePredictLoop callback.

usePredictLoop(room, onSteps, options?)

Owns the per-frame driver: calls predict.tick(now) once per frame and hands you the number of fixed steps due — stage + send exactly that many inputs inside the callback ("send before you read"). By default it runs its own requestAnimationFrame; with { external: true } it returns a drive(now?) function to call from a frame loop you already have:

// React Three Fiber — drive at an early priority so ticks precede reads:
const drive = usePredictLoop(onSteps, { external: true });
useFrame(() => drive(), -1);

useReconciler(room, select, options)

Active prediction for the entity you control — predict.reconciler() with the lifecycle handled: waits for the entity to spawn (select: (state, room) => instance), recreates the controller if the server replaces the instance, and disposes on unmount. options.input defaults to the room's handle.

const me = useReconciler(
  (state, room) => state.players.get(room.sessionId),
  { step: (ctx, p, cmd) => applyInput(p, cmd, ctx.dt), smoothing: 15, snap: 5 },
);

useAttachAll(room, key, config, deps?)

Passive smoothing for a root-state collection — predict.attachAll() as an effect, detached on unmount. Attach angular fields in a separate call:

useAttachAll("players", { mode: "lerp", fields: ["x", "y", "z"] });
useAttachAll("players", { mode: "lerp", fields: ["heading"], angle: true });

useEventChannel(room, options)

Optimistic event channel — predict.defineEvent() with teardown on unmount, plus reactivity: the calling component re-renders on predict/confirm/reject/unpredicted, so flag-shaped derives work in plain render code:

const pickups = useEventChannel({
  confirmOn: { collection: "items", field: "alive", equals: false },
});
const hidden = !item.alive || pickups?.has(id);

useEntityInstance(room, select) / useSessionEntity(room, collectionKey)

Select a decoded schema instance (what the Predict APIs key off), re-rendering only when its identity changes — never on field updates. useSessionEntity is the common case: your own entity by sessionId.

getSchemaInstance(snapshot)

Bridges useRoomState snapshots back to their decoded instance, for predict.value() reads on entities you render from snapshot lists:

const source = getSchemaInstance(playerSnapshot);
useFrame(() => { ref.current.position.x = predict.value(source, "x"); });

useInputBuffer()

The "buffer, then consume" input recipe as a hook: press() in the React event handler, consume() inside the send loop so a tap lands on exactly one fixed step — never lost on a 0-step frame, never doubled on a multi-step frame.

For a complete working example (R3F + prediction + remote lerp), see r3f-lobby-car-prototype.

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).

Credits

Inspiration and previous work by @pedr0fontoura — use-colyseus. Rewrite and new useRoomState() made by @FTWinston.

版本与发布记录

@colyseus/react 共发布 21 个版本,首次发布于 2026/02/10;过去 12 个月发布了 21 个版本。

版本发布日期距今
0.18.4latest2026/10/031 个月前
0.18.32026/10/031 个月前
0.18.22026/08/262 个月前
0.1.192026/08/182 个月前
0.1.182026/08/172 个月前
0.1.172026/06/224 个月前

关于 @colyseus/react 的常见问题

@colyseus/react 是什么?

React hooks for Colyseus multiplayer applications.

如何安装 @colyseus/react?

在项目目录运行 npm install @colyseus/react,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(>=18.3.1)、@colyseus/sdk(^0.18.2)、@colyseus/schema(^5.0.8)、@colyseus/shared-types(^0.18.1)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@colyseus/react 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@colyseus/react 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@colyseus/react 可以商用吗?

@colyseus/react 使用 未声明 许可。package.json 没有声明许可证。没有许可证的代码默认保留全部权利,商用前请联系作者或查看仓库中的 LICENSE 文件。以上为概括,不构成法律意见,请以许可原文为准。

@colyseus/react 的下载量有多少?

根据 npm 官方下载统计,@colyseus/react 在 2026/10/11 – 2026/11/09 这 30 天内被下载 2,731 次,日均约 91 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@colyseus/react 还在维护吗?

最新版本 0.18.4 发布于 2026/10/03(1 个月前);至今共发布 21 个版本,其中过去 12 个月发布了 21 个;首次发布于 2026/02/10。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。