LogoRobo.js

SyncBox

Declarative sync container with locking, interpolation, and optimistic updates.

SyncBox is a declarative component that synchronizes arbitrary state across clients. It wraps useSyncState with additional features: per-field throttling, exclusive locking, smooth interpolation for remote updates, optimistic state updates, and conflict resolution.

Props

PropTypeDefaultDescription
id(string | null)[](required)Key suffix (combined with zone prefix)
initialStateTundefinedInitial state value
childrenReactNode | (state, setState, status, context, lock?) => ReactNodeundefinedChildren or render function
throttlenumber | { [field]: number }undefinedThrottle setState calls (ms), global or per-field
lockablebooleanfalseEnable exclusive ownership mode
interpolate{ [field]: number }undefinedLerp factor per field for smooth remote updates (0-1)
onStateChange(state: T, prev: T | undefined) => voidundefinedCalled when synced state changes
onSyncStatusChange(status: SyncStatus) => voidundefinedCalled when sync status changes
onConflict(local: T, remote: T) => TundefinedResolve conflicts between optimistic and remote state
asElementType | null'div'Wrapper element (null for no wrapper)
styleCSSPropertiesundefinedStyles for wrapper
classNamestringundefinedClass name for wrapper

Render function

The recommended approach. Pass a function as children to access state, setter, sync status, context, and lock info.

import { SyncBox } from '@robojs/sync'

function ScoreBoard() {
  return (
    <SyncBox id={['score']} initialState={{ red: 0, blue: 0 }}>
      {(state, setState, status, context) => (
        <div>
          <p>Red: {state?.red} | Blue: {state?.blue}</p>
          {status.synced ? null : <p>Loading...</p>}
          <button onClick={() => setState({ red: (state?.red ?? 0) + 1 })}>
            +Red
          </button>
        </div>
      )}
    </SyncBox>
  )
}
import { SyncBox } from '@robojs/sync'

function ScoreBoard() {
  return (
    <SyncBox id={['score']} initialState={{ red: 0, blue: 0 }}>
      {(state, setState, status, context) => (
        <div>
          <p>Red: {state?.red} | Blue: {state?.blue}</p>
          {status.synced ? null : <p>Loading...</p>}
          <button onClick={() => setState({ red: (state?.red ?? 0) + 1 })}>
            +Red
          </button>
        </div>
      )}
    </SyncBox>
  )
}

The render function receives:

ParameterTypeDescription
stateT | undefinedCurrent synced state
setStateSyncBoxSetState<T>Update state with optional options
statusSyncStatusSync status info
contextSyncContextRoom context
lockLockContext | undefinedLock info (when lockable=true)

Sync status

FieldTypeDescription
syncedbooleanInitial state received from server
syncingbooleanUpdate in progress
stalebooleanState may be outdated
lastSyncedAtnumber | undefinedTimestamp of last successful sync

Throttling

Control how often state updates are sent over the network.

Global throttle

<SyncBox id={['cursor']} initialState={{ x: 0, y: 0 }} throttle={16}>
  {(state, setState) => (
    <div onMouseMove={(e) => setState({ x: e.clientX, y: e.clientY })}>
      Cursor: {state?.x}, {state?.y}
    </div>
  )}
</SyncBox>
<SyncBox id={['cursor']} initialState={{ x: 0, y: 0 }} throttle={16}>
  {(state, setState) => (
    <div onMouseMove={(e) => setState({ x: e.clientX, y: e.clientY })}>
      Cursor: {state?.x}, {state?.y}
    </div>
  )}
</SyncBox>

Per-field throttle

<SyncBox
  id={['player']}
  initialState={{ x: 0, y: 0, name: '' }}
  throttle={{ x: 16, y: 16, name: 500 }}
>
  {/* Position updates at 60fps, name updates at most every 500ms */}
</SyncBox>
<SyncBox
  id={['player']}
  initialState={{ x: 0, y: 0, name: '' }}
  throttle={{ x: 16, y: 16, name: 500 }}
>
  {/* Position updates at 60fps, name updates at most every 500ms */}
</SyncBox>

Per-call throttle override

setState({ x: newX }, { throttle: 32 }) // Override for this specific call
setState({ x: newX }, { throttle: 32 }) // Override for this specific call

Lockable mode

Enable exclusive ownership — only one client can modify state at a time.

<SyncBox id={['ball']} initialState={{ x: 0, y: 0 }} lockable>
  {(state, setState, status, context, lock) => (
<div
  onMouseDown={() => lock?.lock()}
  onMouseUp={() => lock?.unlock()}
  style={{
    cursor: lock?.isLocked
      ? lock.isLockHolder ? 'grabbing' : 'not-allowed'
      : 'grab'
  }}
>
  Ball at {state?.x}, {state?.y}
</div>
  )}
</SyncBox>
<SyncBox id={['ball']} initialState={{ x: 0, y: 0 }} lockable>
  {(state, setState, status, context, lock) => (
<div
  onMouseDown={() => lock?.lock()}
  onMouseUp={() => lock?.unlock()}
  style={{
    cursor: lock?.isLocked
      ? lock.isLockHolder ? 'grabbing' : 'not-allowed'
      : 'grab'
  }}
>
  Ball at {state?.x}, {state?.y}
</div>
  )}
</SyncBox>

LockContext

FieldTypeDescription
isLockedbooleanWhether anyone holds the lock
lockedBystring | nullLock holder's client ID
isLockHolderbooleanWhether current client holds the lock
lock() => voidAcquire the lock
unlock() => voidRelease the lock

Interpolation

Smooth out remote state updates with linear interpolation. Each field gets a lerp factor between 0 and 1 (lower = smoother but slower convergence).

<SyncBox
  id={['cursor']}
  initialState={{ x: 0.5, y: 0.5 }}
  interpolate={{ x: 0.15, y: 0.15 }}
>
  {(state) => <Cursor x={state?.x} y={state?.y} />}
</SyncBox>
<SyncBox
  id={['cursor']}
  initialState={{ x: 0.5, y: 0.5 }}
  interpolate={{ x: 0.15, y: 0.15 }}
>
  {(state) => <Cursor x={state?.x} y={state?.y} />}
</SyncBox>

Interpolation only applies to remote updates. The lock holder (if using lockable mode) always sees instant updates.

Optimistic updates

Apply state locally before the server confirms. Useful for responsive UIs where you want immediate feedback.

<SyncBox id={['counter']} initialState={{ count: 0 }}>
  {(state, setState) => (
<button onClick={() => setState({ count: (state?.count ?? 0) + 1 }, { optimistic: true })}>
  {state?.count}
</button>
  )}
</SyncBox>
<SyncBox id={['counter']} initialState={{ count: 0 }}>
  {(state, setState) => (
<button onClick={() => setState({ count: (state?.count ?? 0) + 1 }, { optimistic: true })}>
  {state?.count}
</button>
  )}
</SyncBox>

Conflict resolution

When a remote update arrives during an optimistic update, the onConflict callback lets you decide which state wins.

<SyncBox
  id={['score']}
  initialState={{ value: 0 }}
  onConflict={(local, remote) => ({
    value: Math.max(local.value, remote.value)
  })}
>
  {(state, setState) => (
    <button onClick={() => setState({ value: (state?.value ?? 0) + 1 }, { optimistic: true })}>
      Score: {state?.value}
    </button>
  )}
</SyncBox>
<SyncBox
  id={['score']}
  initialState={{ value: 0 }}
  onConflict={(local, remote) => ({
    value: Math.max(local.value, remote.value)
  })}
>
  {(state, setState) => (
    <button onClick={() => setState({ value: (state?.value ?? 0) + 1 }, { optimistic: true })}>
      Score: {state?.value}
    </button>
  )}
</SyncBox>

No wrapper element

Use as={null} to render children directly without a wrapper <div>.

<SyncBox as={null} id={['input']} initialState={{ value: '' }}>
  {(state, setState) => (
    <input
      value={state?.value ?? ''}
      onChange={(e) => setState({ value: e.target.value })}
    />
  )}
</SyncBox>
<SyncBox as={null} id={['input']} initialState={{ value: '' }}>
  {(state, setState) => (
    <input
      value={state?.value ?? ''}
      onChange={(e) => setState({ value: e.target.value })}
    />
  )}
</SyncBox>

Imperative handle

Access SyncBox state programmatically via a ref.

import { useRef } from 'react'
import type { SyncBoxHandle } from '@robojs/sync'

function Controller() {
  const boxRef = useRef<SyncBoxHandle<{ count: number }>>(null)

  const reset = () => boxRef.current?.setState({ count: 0 })
  const status = boxRef.current?.getSyncStatus()

  return (
    <>
      <SyncBox ref={boxRef} id={['counter']} initialState={{ count: 0 }}>
        {(state) => <p>{state?.count}</p>}
      </SyncBox>
      <button onClick={reset}>Reset</button>
    </>
  )
}
import { useRef } from 'react'

function Controller() {
  const boxRef = useRef(null)

  const reset = () => boxRef.current?.setState({ count: 0 })
  const status = boxRef.current?.getSyncStatus()

  return (
    <>
      <SyncBox ref={boxRef} id={['counter']} initialState={{ count: 0 }}>
        {(state) => <p>{state?.count}</p>}
      </SyncBox>
      <button onClick={reset}>Reset</button>
    </>
  )
}

SyncBoxHandle

MethodTypeDescription
getState()() => T | undefinedGet current state
setState(...)SyncBoxSetState<T>Update state
getSyncStatus()() => SyncStatusGet sync status
lockLockContext | undefinedLock context (when lockable=true)

Next steps

On this page