useSyncState
Synchronize state across clients in real-time with a React hook.
The core hook for real-time state sync. Works like React's useState but shares state across all clients watching the same key via WebSockets.
Signature
function useSyncState<T, ClientData = unknown>(
initialState: T,
key: (string | null)[]
): readonly [T, (newState: Partial<T> | ((prev: T) => T)) => void, SyncContext<ClientData>]function useSyncState(
initialState,
key
)
// Returns: [state, setState, context]Parameters
| Parameter | Type | Description |
|---|---|---|
initialState | T | Default state before any server value arrives |
key | (string | null)[] | Scope key. Components sharing the same normalized key share state. |
Return value
| Index | Type | Description |
|---|---|---|
[0] | T | Current synced state |
[1] | (value: Partial<T> | ((prev: T) => T)) => void | Setter function — accepts partial objects or updater functions |
[2] | SyncContext<ClientData> | Context with clients, clientId, isHost, broadcast, send |
Basic usage
import { useSyncState } from '@robojs/sync'
function Counter() {
const [count, setCount] = useSyncState(0, ['counter'])
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>
}import { useSyncState } from '@robojs/sync'
function Counter() {
const [count, setCount] = useSyncState(0, ['counter'])
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>
}Updater functions
setCount((prev) => prev + 1)setCount((prev) => prev + 1)When using an updater function, the previous value comes from the server cache rather than React state, so it's always the latest synced value.
Object state
const [player, setPlayer] = useSyncState({ x: 0, y: 0, score: 0 }, ['player', odId])
// Partial update — only sends changed fields
setPlayer({ x: 10, y: 20 })const [player, setPlayer] = useSyncState({ x: 0, y: 0, score: 0 }, ['player', odId])
// Partial update — only sends changed fields
setPlayer({ x: 10, y: 20 })Context
The third return value provides room context.
const [state, setState, context] = useSyncState(initialState, ['game', roomId])
console.log(context.clientId) // Your unique client ID
console.log(context.isHost) // true if you're the room host
console.log(context.clients) // Array of all connected clients
context.broadcast({ type: 'ping' }) // Send to all clients
context.send(targetId, { text: 'hello' }) // Send to specific clientconst [state, setState, context] = useSyncState(initialState, ['game', roomId])
console.log(context.clientId) // Your unique client ID
console.log(context.isHost) // true if you're the room host
console.log(context.clients) // Array of all connected clients
context.broadcast({ type: 'ping' }) // Send to all clients
context.send(targetId, { text: 'hello' }) // Send to specific clientThe context.clients array contains Client<ClientData> objects. Each client has an id string and an optional data field populated from the clientData prop on SyncContextProvider.
Key scoping
Keys determine which components share state. Keys are normalized to dot-notation internally.
// These two components share the same state
const [a] = useSyncState(0, ['room', '123', 'score'])
const [b] = useSyncState(0, ['room', '123', 'score'])
// This one has separate state
const [c] = useSyncState(0, ['room', '456', 'score'])// These two components share the same state
const [a] = useSyncState(0, ['room', '123', 'score'])
const [b] = useSyncState(0, ['room', '123', 'score'])
// This one has separate state
const [c] = useSyncState(0, ['room', '456', 'score'])Avoid dots in individual key segments. ['a.b'] normalizes to the same key as ['a', 'b'], which can cause unintended collisions.
Offline behavior
When the WebSocket is disconnected, updates are queued in memory and flushed on reconnection. Queued state is lost if the page refreshes before reconnecting.
