LogoRobo.js

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

ParameterTypeDescription
initialStateTDefault state before any server value arrives
key(string | null)[]Scope key. Components sharing the same normalized key share state.

Return value

IndexTypeDescription
[0]TCurrent synced state
[1](value: Partial<T> | ((prev: T) => T)) => voidSetter 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 client
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 client

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

Next Steps

On this page