React Hooks
All hooks are generated by createZocketReact<typeof app>() and are fully typed from your actor definitions.
useClient
Section titled “useClient”Returns the typed client from context:
const client = useClient();Throws if used outside <ZocketProvider>.
useActor
Section titled “useActor”Get a stable, ref-counted ActorHandle for a specific actor instance:
const room = useActor("chat", roomId);actorName— must match a key in your app’sactors(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>;}Key Identity
Section titled “Key Identity”The handle is recreated when actorName or actorId changes. The previous handle is disposed.
StrictMode Compatibility
Section titled “StrictMode Compatibility”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.
useEvent
Section titled “useEvent”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])
Type Inference Note
Section titled “Type Inference Note”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.
useActorState
Section titled “useActorState”Subscribe to actor state with an optional selector:
// Full stateconst state = useActorState(room);
// With selector — only re-renders when messages changeconst messages = useActorState(room, (s) => s.messages);
// Derived valueconst messageCount = useActorState(room, (s) => s.messages.length);How It Works
Section titled “How It Works”- Uses
useSyncExternalStoreunder the hood - Subscribes to the handle’s
StateStore - If a selector is provided, it runs locally on each state update
- Uses shallow compare caching — the selector result is only updated when the raw state reference changes
- Returns
undefineduntil the first state snapshot is received
Selector Best Practices
Section titled “Selector Best Practices”// Good — simple property accessconst phase = useActorState(room, (s) => s.phase);
// Good — derived computationconst 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 useMemoFull Example
Section titled “Full Example”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> );}