Skip to content

Actor Handles

An Actor Handle is the client-side proxy for a single actor instance. It provides typed methods, event subscriptions, state subscriptions, and lifecycle management.

const room = client.chat("room-1");

This returns a typed ActorHandle<typeof ChatRoom> — all methods, events, and state types are inferred.

Methods are accessed directly on the handle. They return promises:

// Method with input
await room.sendMessage({ text: "hello" });
// Method without input
const count = await counter.increment();
// Method with return value
const { playerId, color } = await game.join({ name: "Alice" });

Use .on() to listen for typed events:

const unsubscribe = room.on("newMessage", (payload) => {
// payload is typed: { text: string; from: string }
console.log(`${payload.from}: ${payload.text}`);
});
// Later:
unsubscribe();

Event subscriptions are lazy — the client only sends an event:sub message to the server when the first listener is added. When all listeners are removed, it sends event:unsub.

The .state object provides subscribe() and getSnapshot():

// Subscribe to state changes
const unsub = room.state.subscribe((state) => {
console.log("Current messages:", state.messages);
});
// Read current state synchronously
const current = room.state.getSnapshot();

Like events, state subscriptions are lazy. The first subscribe() sends state:sub to the server, which triggers an immediate state:snapshot response. Subsequent mutations on the server are received as state:patch messages.

  1. Server mutates state via Immer → generates JSON patches
  2. Server sends state:patch to all state subscribers
  3. Client’s StateStore applies patches to local state copy
  4. All registered listeners are notified

Call $dispose() when you’re done with a handle:

room.$dispose();

This decrements the reference count. When the ref count hits zero (after a one-tick delay), the handle:

  1. Unsubscribes from events on the server
  2. Unsubscribes from state on the server
  3. Rejects all pending RPCs with "ActorHandle disposed"
  4. Clears all listeners

Multiple consumers can share the same handle. Each call to client.chat("room-1") increments the ref count. Each $dispose() decrements it. The handle is only truly disposed when the count reaches zero.

The one-tick delay on final disposal ensures React StrictMode’s temporary unmount/remount cycle doesn’t kill shared handles.

PropertyTypeDescription
$actorNamestringThe actor name (e.g. "chat")
$actorIdstringThe instance ID (e.g. "room-1")
$dispose()() => voidDecrement ref count

If the WebSocket disconnects, all pending RPCs on the handle are rejected:

try {
await room.sendMessage({ text: "hello" });
} catch (err) {
console.error(err.message); // "WebSocket closed"
}

If an RPC timeout is configured on the client, individual RPCs will reject after the timeout:

// err.message: 'RPC "sendMessage" timed out after 5000ms'