Skip to main content
The synced() function creates an observable that automatically syncs with local persistence and remote data sources. It’s a simplified wrapper around syncObservable() for common use cases.

Usage

import { synced } from '@legendapp/state/sync'

const data$ = synced({
  get: () => fetch('/api/data').then(r => r.json()),
  set: ({ value }) => fetch('/api/data', {
    method: 'POST',
    body: JSON.stringify(value)
  }),
  persist: {
    name: 'myData',
    plugin: ObservablePersistLocalStorage
  }
})

Parameters

options
SyncedOptions<TRemote, TLocal>
required
Configuration for syncing behavior

SyncedOptions

get
(params: SyncedGetParams<TRemote>) => Promise<TRemote> | TRemote
Function to fetch data from remote source
get: async ({ lastSync, value, onError }) => {
  const response = await fetch('/api/data')
  return response.json()
}
set
(params: SyncedSetParams<TRemote>) => void | Promise<any>
Function to save changes to remote source
set: async ({ value, changes }) => {
  await fetch('/api/data', {
    method: 'POST',
    body: JSON.stringify(value)
  })
}
subscribe
(params: SyncedSubscribeParams<TRemote>) => (() => void) | void
Function to subscribe to real-time updates
subscribe: ({ update }) => {
  const unsubscribe = socket.on('data', (data) => {
    update({ value: data })
  })
  return unsubscribe
}
persist
PersistOptions
Local persistence configuration. See persist plugins for details.
initial
TLocal | (() => TLocal)
Initial value before data loads
transform
SyncTransform<TLocal, TRemote>
Transform data between local and remote formats
mode
'set' | 'assign' | 'merge' | 'append' | 'prepend'
default:"set"
How to apply remote data to the observable
  • set: Replace entire value
  • assign: Shallow merge (Object.assign)
  • merge: Deep merge
  • append: Add to end of array
  • prepend: Add to beginning of array
debounceSet
number
Debounce remote saves by this many milliseconds
syncMode
'auto' | 'manual'
default:"auto"
  • auto: Sync automatically when observable is accessed
  • manual: Only sync when sync() is called
retry
RetryOptions
Retry configuration for failed operations
waitFor
Promise<any> | Observable<any> | (() => any)
Wait for condition before syncing get/subscribe
waitForSet
(params: WaitForSetFnParams) => any
Wait for condition before saving changes
onBeforeGet
(params) => void
Called before fetching remote data
onBeforeGet: ({ value, lastSync, pendingChanges, cancel, clearPendingChanges, resetCache }) => {
  // Optionally cancel the get
  if (shouldSkip) {
    cancel = true
  }
}
onBeforeSet
(params: { cancel: boolean }) => void
Called before saving changes
onAfterSet
() => void
Called after changes are saved
onError
(error: Error, params: SyncedErrorParams) => void
Error handler for sync operations
onError: (error, { source, type, retry, revert }) => {
  console.error(`${source} error:`, error)
  // Optionally revert changes
  if (shouldRevert) revert?.()
}

Return Value

Returns a Synced<TLocal> observable with the following characteristics:
  • Automatically loads from local persistence on first access
  • Syncs with remote data source based on syncMode
  • Tracks sync state via syncState(observable)
  • Persists changes locally and remotely

SyncedGetParams

Parameters passed to the get function:
node
NodeInfo
Internal node information
value$
Observable<T>
The observable being synced
refresh
() => void
Function to trigger a refresh
value
any
Current value of the observable
lastSync
number | undefined
Timestamp of last successful sync
updateLastSync
(lastSync: number) => void
Update the last sync timestamp
mode
GetMode
How to apply the loaded data
onError
(error: Error, params: SyncedErrorParams) => void
Error handler
retryNum
number
Current retry attempt number
cancelRetry
boolean
Set to true to cancel retrying

SyncedSetParams

Parameters passed to the set function:
node
NodeInfo
Internal node information
value$
Observable<T>
The observable being synced
refresh
() => void
Function to trigger a refresh
value
T
The value to save
changes
ChangeWithPathStr[]
Array of changes that triggered this saveEach change contains:
  • path: Array of keys to the changed value
  • pathTypes: Types of each path segment
  • valueAtPath: New value at this path
  • prevAtPath: Previous value at this path
  • pathStr: String representation of path
update
(params: UpdateSetFnParams) => void
Update the observable with changes from the server
update({
  value: serverResponse,
  mode: 'merge',
  changes: [/* optional changes */]
})
onError
(error: Error, params: SyncedErrorParams) => void
Error handler
retryNum
number
Current retry attempt number
cancelRetry
boolean
Set to true to cancel retrying

SyncedSubscribeParams

Parameters passed to the subscribe function:
node
NodeInfo
Internal node information
value$
Observable<T>
The observable being synced
refresh
() => void
Function to trigger a refresh
lastSync
number | undefined
Timestamp of last successful sync
update
(params: UpdateFnParams) => void
Update the observable with new data
update({
  value: newData,
  lastSync: Date.now(),
  mode: 'merge'
})
onError
(error: Error) => void
Error handler

Examples

Simple GET/SET

const user$ = synced({
  get: () => fetch('/api/user').then(r => r.json()),
  set: ({ value }) => fetch('/api/user', {
    method: 'PUT',
    body: JSON.stringify(value)
  }),
  initial: { name: '', email: '' },
  persist: {
    name: 'user',
    plugin: ObservablePersistLocalStorage
  }
})

With Transform

const data$ = synced({
  get: () => api.getData(),
  transform: {
    load: (remote) => {
      // Convert dates from strings
      return {
        ...remote,
        createdAt: new Date(remote.createdAt)
      }
    },
    save: (local) => {
      // Convert dates to ISO strings
      return {
        ...local,
        createdAt: local.createdAt.toISOString()
      }
    }
  }
})

With Real-time Updates

const messages$ = synced({
  get: () => api.getMessages(),
  set: ({ value }) => api.sendMessage(value),
  subscribe: ({ update }) => {
    const unsubscribe = socket.on('message', (msg) => {
      update({ value: [msg], mode: 'append' })
    })
    return () => unsubscribe()
  },
  initial: []
})

Function Shorthand

// If you only need get, you can pass a function directly
const data$ = synced(() => fetch('/api/data').then(r => r.json()))

// Equivalent to:
const data$ = synced({
  get: () => fetch('/api/data').then(r => r.json())
})

See Also

Build docs developers (and LLMs) love