Skip to content

React Hooks

All hooks are generated by createZocketReact<typeof app>() and are fully typed from your actor definitions.

Returns the typed client from context:

const client = useClient();

Throws if used outside <ZocketProvider>.

Get a stable, ref-counted ActorHandle for a specific actor instance:

const room = useActor("chat", roomId);
  • actorName — must match a key in your app’s actors (typed and autocompleted)
  • actorId — the instance identifier (e.g. "room-1")

The handle is ref-counted — multiple components can share the same actor without one unmount killing the other’s connection. On unmount, $dispose() is called automatically.

function ChatRoom({ roomId }: { roomId: string }) {
const room = useActor("chat", roomId);
// Call methods
const handleSend = async (text: string) => {
await room.sendMessage({ from: "Alice", text });
};
// Subscribe to events
room.on("newMessage", (msg) => {
console.log(msg);
});
return <button onClick={() => handleSend("hi")}>Send</button>;
}

The handle is recreated when actorName or actorId changes. The previous handle is disposed.

React StrictMode unmounts and remounts components. Zocket handles this by deferring disposal by one tick — the remount re-retains the handle before the deferred dispose fires.

Subscribe to an actor event with automatic cleanup:

useEvent(room, "correctGuess", (payload) => {
// payload is typed from your event schema
toast(`${payload.name} guessed "${payload.word}"!`);
});
  • Subscribes on mount, unsubscribes on unmount
  • The callback ref is kept up-to-date (no stale closures)
  • Equivalent to useEffect(() => room.on("event", cb), [room])

For full type inference on event names and payloads, use room.on() directly. The useEvent hook accepts string for the event name to keep the implementation simple — but the callback payload is still inferred from the handle type.

Subscribe to actor state with an optional selector:

// Full state
const state = useActorState(room);
// With selector — only re-renders when messages change
const messages = useActorState(room, (s) => s.messages);
// Derived value
const messageCount = useActorState(room, (s) => s.messages.length);
  1. Uses useSyncExternalStore under the hood
  2. Subscribes to the handle’s StateStore
  3. If a selector is provided, it runs locally on each state update
  4. Uses shallow compare caching — the selector result is only updated when the raw state reference changes
  5. Returns undefined until the first state snapshot is received
// Good — simple property access
const phase = useActorState(room, (s) => s.phase);
// Good — derived computation
const isMyTurn = useActorState(room, (s) => s.drawerId === myId);
// Avoid — creating new arrays on every state change
// (will cause re-renders because the reference changes)
const sorted = useActorState(room, (s) =>
[...s.players].sort((a, b) => b.score - a.score)
);
// Instead, sort in the component with useMemo
import { ZocketProvider, useActor, useEvent, useActorState, client } from "./zocket";
function GameRoom({ roomId }: { roomId: string }) {
const room = useActor("draw", roomId);
const phase = useActorState(room, (s) => s.phase);
const players = useActorState(room, (s) => s.players);
useEvent(room, "correctGuess", ({ name, word }) => {
alert(`${name} guessed "${word}"!`);
});
return (
<div>
<p>Phase: {phase}</p>
<ul>
{players?.map((p) => (
<li key={p.id}>{p.name} — {p.score} pts</li>
))}
</ul>
</div>
);
}
export function App() {
return (
<ZocketProvider client={client}>
<GameRoom roomId="room-1" />
</ZocketProvider>
);
}