LogoRobo.js

Sync API

Complete @robojs/sync API reference

This is an API reference. For a tutorial on adding multiplayer to your activity, see Multiplayer.

Complete API reference for @robojs/sync, the real-time state synchronization plugin for Discord Activities.

Choosing the Right Hook

HookUse CasePersists?Example
useSyncStateShared state across clientsYes (in-memory)Game scores, shared counters, form data
useSyncBroadcastEphemeral messagesNoReactions, typing indicators, sound effects
useSyncCallServer-validated actionsNo (returns result)Move validation, purchases, admin actions
useSyncPresenceWho's online and their statusAuto-managedPlayer list, "user is typing", online indicators
useSyncCursorCursor position trackingNoCollaborative drawing, pointer trails
useSyncDragDraggable elementsYes (position)Card games, drag-and-drop boards, sliders

Start with useSyncState — it covers most use cases. Reach for specialized hooks when you need ephemeral messaging (useSyncBroadcast), server authority (useSyncCall), or optimized tracking (useSyncPresence, useSyncCursor, useSyncDrag).

Use hooks for simple cases. For complex UIs with many independent synchronized elements, use SyncZone and SyncBox — they provide hierarchical key management and declarative state containers without needing to wire up individual hooks.

Core Hooks

useSyncState

Synchronized state shared across all clients subscribed to the same key. Works like React's useState, but updates are broadcast to every connected client.

const [state, setState, context] = useSyncState<T>(initialState, key)
const [state, setState, context] = useSyncState(initialState, key)
ReturnTypeDescription
stateTCurrent synchronized state
setState(newState: Partial<T> | ((prev: T) => T)) => voidUpdate state with a partial object or updater function
contextSyncContextClient awareness and messaging context

Keys are arrays that scope the state. Components using the same key share the same synchronized value.

src/app/Game.tsx
const [position, setPosition, context] = useSyncState({ x: 0, y: 0 }, ['player-position'])

setPosition({ x: 10 })
setPosition((prev) => ({ ...prev, y: prev.y + 1 }))
src/app/Game.jsx
const [position, setPosition, context] = useSyncState({ x: 0, y: 0 }, ['player-position'])

setPosition({ x: 10 })
setPosition((prev) => ({ ...prev, y: prev.y + 1 }))

useSyncBroadcast

Ephemeral messaging for fire-and-forget payloads that do not persist as state. Broadcasts do not trigger re-renders in other components.

const { broadcast, send, context } = useSyncBroadcast(handler, key)
const { broadcast, send, context } = useSyncBroadcast(handler, key)
ReturnTypeDescription
broadcast(payload: unknown) => voidSend to all clients in the room
send(clientId: string, payload: unknown) => voidSend to a specific client
contextSyncContextClient awareness and messaging context

Use cases include reactions, typing indicators, and cursor movements.

src/app/Chat.tsx
const { broadcast, send, context } = useSyncBroadcast((payload, { client }) => {
  console.log(client.id, 'sent', payload)
}, ['chat-room'])

broadcast('Hello everyone!')
send(targetClientId, 'Private message')
src/app/Chat.jsx
const { broadcast, send, context } = useSyncBroadcast((payload, { client }) => {
  console.log(client.id, 'sent', payload)
}, ['chat-room'])

broadcast('Hello everyone!')
send(targetClientId, 'Private message')

useSyncCall

Server-authoritative RPC for operations where the server should validate and process the action rather than applying direct state updates.

const call = useSyncCall(key)
const call = useSyncCall(key)

The returned function sends a method call to the server and returns a promise with the result.

call<Payload, Result>(method: string, payload?: Payload): Promise<CallResult<Result>>
call(method, payload) // Returns: Promise<CallResult>
FieldTypeDescription
successbooleanWhether the call succeeded
resultT | undefinedReturn value from the server handler
errorstring | undefinedError message if the call failed

Server handlers live in /src/sync/ as file-based routes.

src/app/Game.tsx
const call = useSyncCall(['game', roomId])

const result = await call('move', { x: 10, y: 20 })
if (!result.success) {
  console.log('Rejected:', result.error)
}
src/app/Game.jsx
const call = useSyncCall(['game', roomId])

const result = await call('move', { x: 10, y: 20 })
if (!result.success) {
  console.log('Rejected:', result.error)
}

useSyncContext

Access the sync context for a given key without managing state. Returns client awareness, host status, and messaging capabilities.

const context = useSyncContext(key)
const context = useSyncContext(key)

Supports optional callbacks for connection events:

src/app/Room.tsx
const context = useSyncContext({
  onConnect: (client) => console.log(client.id, 'joined'),
  onDisconnect: (client) => console.log(client.id, 'left')
}, ['game-room'])
src/app/Room.jsx
const context = useSyncContext({
  onConnect: (client) => console.log(client.id, 'joined'),
  onDisconnect: (client) => console.log(client.id, 'left')
}, ['game-room'])

Presence and Cursors

useSyncPresence

Track participants and their metadata in real time. Handles heartbeats and stale detection automatically.

const result = useSyncPresence<T>(key, options?)
const result = useSyncPresence(key, options)

PresenceOptions

OptionTypeDefaultDescription
initialPresenceT{}Initial presence data
staleTimeoutnumber5000Ms before marking stale
heartbeatIntervalnumber2000Ms between heartbeats

PresenceResult

FieldTypeDescription
participantsParticipant<T>[]All participants with presence data
updatePresence(update: Partial<T> | ((prev: T) => T)) => voidUpdate current client's presence
clientIdstringCurrent client's ID
isHostbooleanWhether current client is host
contextSyncContextFull sync context

Participant

FieldTypeDescription
clientIdstringClient ID
userClientClient metadata
presenceTPresence data
isStalebooleanWhether presence is stale
isYoubooleanWhether this is the current client
src/app/Room.tsx
const { participants, updatePresence } = useSyncPresence(['room', roomId], {
  initialPresence: { status: 'online', activity: 'idle' }
})

updatePresence({ activity: 'typing' })
src/app/Room.jsx
const { participants, updatePresence } = useSyncPresence(['room', roomId], {
  initialPresence: { status: 'online', activity: 'idle' }
})

updatePresence({ activity: 'typing' })

useSyncCursor

Broadcast cursor positions in real time. Optimized to avoid rerenders for your own cursor movement.

const result = useSyncCursor<ClientData>(key, options?)
const result = useSyncCursor(key, options)

CursorOptions

OptionTypeDefaultDescription
throttlenumber16Ms between updates (~60fps)
normalizebooleantrueConvert to 0-1 viewport range
hideOnLeavebooleantrueHide cursor when mouse leaves
inactiveTimeoutnumber3000Ms before removing inactive cursors
autoTrackbooleanfalseAutomatically track mouse movement

CursorResult

FieldTypeDescription
cursorsRemoteCursor[]All cursors including yours
remoteCursorsRemoteCursor[]Only other users' cursors
updatePosition(pos: { x: number; y: number; active?: boolean }) => voidManually update cursor position
clientIdstringCurrent client's ID

RemoteCursor

FieldTypeDescription
clientIdstringCursor owner's client ID
userClientClient metadata
positionCursorPosition{ x, y, active }
isYoubooleanWhether this is your cursor
src/app/Canvas.tsx
const { remoteCursors } = useSyncCursor(['room', roomId], { autoTrack: true })
src/app/Canvas.jsx
const { remoteCursors } = useSyncCursor(['room', roomId], { autoTrack: true })

SyncCursors

Drop-in component for rendering remote cursors. Handles mouse tracking, throttling, and positioning automatically.

src/app/Activity.tsx
<SyncCursors roomKey={['room', roomId]} />
src/app/Activity.jsx
<SyncCursors roomKey={['room', roomId]} />

Props

PropTypeDefaultDescription
roomKey(string | null)[]requiredRoom key for cursor sync
throttlenumber16Ms between updates
renderCursor(cursor: RemoteCursor) => ReactNodeundefinedCustom cursor renderer
showLabelsbooleantrueShow user labels
labelKeykeyof ClientData | (client: Client) => stringundefinedKey or function for label text
colorFn(clientId: string) => stringhash-basedColor generator from client ID
zIndexnumber9999Z-index for cursor container
src/app/Activity.tsx
<SyncCursors
  roomKey={['room', roomId]}
  renderCursor={(cursor) => (
    <div>
      <CustomCursorIcon color={colorFn(cursor.clientId)} />
      <span>{cursor.user.data?.name}</span>
    </div>
  )}
/>
src/app/Activity.jsx
<SyncCursors
  roomKey={['room', roomId]}
  renderCursor={(cursor) => (
    <div>
      <CustomCursorIcon color={colorFn(cursor.clientId)} />
      <span>{cursor.user.data?.name}</span>
    </div>
  )}
/>

Draggable Elements

useSyncDrag

Synchronized dragging with locking, interpolation, bounds, and gesture detection.

const result = useSyncDrag<T extends DragState>(key, initialState, options?)
const result = useSyncDrag(key, initialState, options)

DragOptions

OptionTypeDefaultDescription
interpolateInterpolateConfigundefinedLerp factors for smooth remote updates
throttlenumber16Ms between updates
boundsDragBoundsundefinedPosition constraints (minX, maxX, minY, maxY)
normalizebooleantrueUse 0-1 viewport coordinates
lockOnDragbooleantrueAcquire lock when dragging starts

DragResult

FieldTypeDescription
stateTCurrent position and custom data
setState(update: Partial<T> | ((prev: T) => T)) => voidUpdate state
isDraggingbooleanWhether current client is dragging
isBeingDraggedbooleanWhether anyone is dragging (locked)
canInteractbooleanWhether current client can interact
dragHandlers{ onMouseDown, onTouchStart }Handlers to spread onto the element
lockLockContextLock/unlock functions
contextSyncContextSync context
src/app/DraggableBall.tsx
function DraggableBall({ id }: { id: string }) {
  const { state, isDragging, canInteract, dragHandlers } = useSyncDrag(
    ['ball', id],
    { x: 0.5, y: 0.5 },
    { interpolate: { x: 0.2, y: 0.2 } }
  )

  return (
    <div
      {...dragHandlers}
      style={{
        position: 'absolute',
        left: `${state.x * 100}%`,
        top: `${state.y * 100}%`,
        cursor: isDragging ? 'grabbing' : canInteract ? 'grab' : 'not-allowed'
      }}
    />
  )
}
src/app/DraggableBall.jsx
function DraggableBall({ id }) {
  const { state, isDragging, canInteract, dragHandlers } = useSyncDrag(
    ['ball', id],
    { x: 0.5, y: 0.5 },
    { interpolate: { x: 0.2, y: 0.2 } }
  )

  return (
    <div
      {...dragHandlers}
      style={{
        position: 'absolute',
        left: `${state.x * 100}%`,
        top: `${state.y * 100}%`,
        cursor: isDragging ? 'grabbing' : canInteract ? 'grab' : 'not-allowed'
      }}
    />
  )
}

SyncDraggable

Declarative component wrapper for synchronized dragging. Handles positioning, locking, interpolation, and touch/mouse events automatically.

src/app/Activity.tsx
<SyncDraggable id={['ball', '1']} initial={{ x: 0.5, y: 0.5 }}>
  <div className="ball">Drag me</div>
</SyncDraggable>
src/app/Activity.jsx
<SyncDraggable id={['ball', '1']} initial={{ x: 0.5, y: 0.5 }}>
  <div className="ball">Drag me</div>
</SyncDraggable>

Props

PropTypeDefaultDescription
id(string | null)[]requiredKey suffix
initialTrequiredInitial state (must include x, y)
interpolateInterpolateConfigundefinedSmooth remote updates
throttlenumber16Ms between updates
boundsDragBoundsundefinedPosition constraints
normalizebooleantrueUse 0-1 coordinates
onDragStart(state: T) => voidundefinedCalled when drag starts
onDrag(state: T) => voidundefinedCalled on each update
onDragEnd(state: T) => voidundefinedCalled when drag ends
styleCSSPropertiesundefinedWrapper styles
classNamestringundefinedWrapper class
asstring | null'div'Wrapper element

Supports render props for custom styling:

src/app/Activity.tsx
<SyncDraggable
  id={['ball', '1']}
  initial={{ x: 0.5, y: 0.5 }}
  interpolate={{ x: 0.2, y: 0.2 }}
  bounds={{ minX: 0.05, maxX: 0.95 }}
>
  {({ isDragging, canInteract }) => (
    <div style={{ cursor: isDragging ? 'grabbing' : canInteract ? 'grab' : 'not-allowed' }}>
      Drag me
    </div>
  )}
</SyncDraggable>
src/app/Activity.jsx
<SyncDraggable
  id={['ball', '1']}
  initial={{ x: 0.5, y: 0.5 }}
  interpolate={{ x: 0.2, y: 0.2 }}
  bounds={{ minX: 0.05, maxX: 0.95 }}
>
  {({ isDragging, canInteract }) => (
    <div style={{ cursor: isDragging ? 'grabbing' : canInteract ? 'grab' : 'not-allowed' }}>
      Drag me
    </div>
  )}
</SyncDraggable>

Zones and Boxes

SyncZone

Hierarchical key prefixing for organizing sync state. Nested zones accumulate their prefixes.

src/app/Game.tsx
<SyncZone id={['game']}>
  <SyncZone id={['board']}>
    <SyncBox id={['piece']} /> {/* key becomes game.board.piece */}
  </SyncZone>
</SyncZone>
src/app/Game.jsx
<SyncZone id={['game']}>
  <SyncZone id={['board']}>
    <SyncBox id={['piece']} /> {/* key becomes game.board.piece */}
  </SyncZone>
</SyncZone>

Props

PropTypeDefaultDescription
id(string | null)[]requiredKey prefix for this zone
hostRules'first' | 'explicit''first'Host determination strategy
hoststring | nullundefinedExplicit host client ID

SyncBox

Declarative sync state container with render function support. Inherits zone prefixes automatically.

src/app/Game.tsx
<SyncBox id={['counter']} initialState={{ count: 0 }}>
  {(state, setState, status, context, lock) => (
    <button onClick={() => setState({ count: (state?.count ?? 0) + 1 })}>
      {state?.count}
    </button>
  )}
</SyncBox>
src/app/Game.jsx
<SyncBox id={['counter']} initialState={{ count: 0 }}>
  {(state, setState, status, context, lock) => (
    <button onClick={() => setState({ count: (state?.count ?? 0) + 1 })}>
      {state?.count}
    </button>
  )}
</SyncBox>

Props

PropTypeDefaultDescription
id(string | null)[]requiredKey suffix
initialStateTundefinedInitial state
lockablebooleanfalseEnable lock mode
interpolateInterpolateConfigundefinedSmooth remote updates
throttleThrottleConfigundefinedUpdate throttling (number or per-field config)
onStateChange(state: T, prevState: T | undefined) => voidundefinedState change callback
onConflict(localState: T, remoteState: T) => TundefinedConflict resolution
aselement | null'div'Wrapper element
styleCSSPropertiesundefinedWrapper styles
classNamestringundefinedWrapper class

The render function receives five arguments:

ArgumentTypeDescription
stateT | undefinedCurrent synchronized state
setStateSyncBoxSetState<T>Update function with optional SetStateOptions
statusSyncStatus{ synced, syncing, stale, lastSyncedAt }
contextSyncContextClient awareness context
lockLockContext | undefinedLock controls (when lockable is enabled)

useZoneContext

Access zone metadata from any component inside a SyncZone. Returns null if not within a zone.

src/app/Game.tsx
const zone = useZoneContext()
src/app/Game.jsx
const zone = useZoneContext()
FieldTypeDescription
prefix(string | null)[]Accumulated key prefix from nested zones
hostIdstringThe host client ID for this zone
isHostbooleanWhether current client is host
connectionStatusConnectionStatus'connecting' | 'connected' | 'disconnected' | 'error'
clientsClient[]Clients subscribed to this zone
broadcast(payload: unknown) => voidSend to all clients in zone
send(clientId: string, payload: unknown) => voidSend to specific client

Server Handlers

File-based handlers in /src/sync/ process server-side logic for sync keys. Each handler file can export:

game.tsHandles game state
ExportDescription
schemaValidation schema for incoming state
validate()Custom validation function
transform()Modify state before broadcasting
onUpdate()Side effects on state change
Named methodsRPC handlers invoked via useSyncCall

Here is a complete example of a server handler that validates moves and tracks game state:

src/sync/game.ts
import type { SyncUpdateContext, SyncCallContext } from '@robojs/sync/server'

// State validation — reject invalid updates
export function validate(context: SyncUpdateContext<{ x: number; y: number }>) {
  if (context.newState.x < 0 || context.newState.x > 100 || context.newState.y < 0 || context.newState.y > 100) {
    return 'Position out of bounds'
  }
  return true
}

// Transform state before broadcasting to other clients
export function transform(context: SyncUpdateContext<{ x: number; y: number }>) {
  return { ...context.newState, lastUpdatedBy: context.client.id }
}

// Side effects on state change
export function onUpdate(context: SyncUpdateContext<{ x: number; y: number }>) {
  console.log('Game state updated:', context.newState)
}

// RPC handler — invoked via useSyncCall(['game'], 'move', payload)
export function move(payload: { x: number; y: number }, context: SyncCallContext) {
  if (payload.x < 0 || payload.y < 0) {
    return { success: false, error: 'Invalid position' }
  }
  return { success: true, result: { x: payload.x, y: payload.y } }
}
src/sync/game.js
// State validation — reject invalid updates
export function validate(context) {
  if (context.newState.x < 0 || context.newState.x > 100 || context.newState.y < 0 || context.newState.y > 100) {
    return 'Position out of bounds'
  }
  return true
}

// Transform state before broadcasting to other clients
export function transform(context) {
  return { ...context.newState, lastUpdatedBy: context.client.id }
}

// Side effects on state change
export function onUpdate(context) {
  console.log('Game state updated:', context.newState)
}

// RPC handler — invoked via useSyncCall(['game'], 'move', payload)
export function move(payload, context) {
  if (payload.x < 0 || payload.y < 0) {
    return { success: false, error: 'Invalid position' }
  }
  return { success: true, result: { x: payload.x, y: payload.y } }
}

SyncContext Object

The SyncContext object is returned by useSyncState, useSyncContext, and useSyncBroadcast.

FieldTypeDescription
clientsClient[]Connected clients
clientIdstringCurrent client's ID
isHostbooleanWhether current client is host
broadcast(payload: unknown) => voidSend to all clients
send(clientId: string, payload: unknown) => voidSend to specific client

Client

FieldTypeDescription
idstringUnique client identifier
dataunknownClient metadata

Next Steps

On this page