Skip to main content

React hooks

import { useCloudItem } from 'react-native-cloud-sync/hooks'

A separate entry point: the store also runs in a saga, background task or plain module, none of which need a renderer.

Every app writes these same hooks and hits the same two bugs - a stale response overwriting a newer one, and setState after unmount - handled once here.

useCloudItem

Binds one key to component state.

import { useCloudItem } from 'react-native-cloud-sync/hooks'

function SettingsScreen () {
const { value, setValue, loading, error } = useCloudItem<Settings>(store, 'settings')

if (loading) return <Spinner />
if (error != null) return <SyncError code={error.code} />

return <SettingsForm value={value} onChange={setValue} />
}
valueParsed value, or null when the key does not exist
loadingTrue until the first read settles, and during refresh()
errorLast read or write failure; cleared by the next success
setValueWrites through, updating local state first
removeDeletes the key
refreshRe-reads

Values go through JSON.parse/JSON.stringify by default; for a plain string, pass identity functions:

useCloudItem<string>(store, 'theme', {
parse: raw => raw,
serialize: value => value,
})

It re-reads when another device writes

watch defaults to true, subscribing to onRemoteChange so another device's edit doesn't need a manual refresh to appear. An empty keys list (account or quota event) still triggers a re-read.

Writes are optimistic and do not revert

setValue updates local state immediately and keeps it on failure, since the outbox usually delivers anyway. A failed read likewise keeps the last known value rather than blanking the screen over a network blip.

Stale responses cannot land

Every read carries a ticket; a stale one - key changed, overtaken, invalidated - is dropped instead of written into state, avoiding a hand-rolled version's flash-of-wrong-value bug on fast navigation.

useCloudItems

The multi-key sibling of useCloudItem. Binds a list of keys with one batched multiGet instead of one useCloudItem per row.

function NotesList ({ noteIds }: { noteIds: string[] }) {
const { values, setValue, loading } = useCloudItems<Note>(store, noteIds)

if (loading) return <Spinner />

return noteIds.map(id => (
<NoteRow key={id} note={values[id]} onChange={next => setValue(id, next)} />
))
}
valuesParsed value per key. A key mapped to null was fetched and doesn't exist; a key absent hasn't been fetched yet
loadingTrue only during the initial fetch of keys, and during refresh()
errorLast read or write failure; cleared by the next success
setValue(key, next) => Promise<void> - writes through, updating local state first
remove(key) => Promise<void>
refreshRe-fetches every key currently tracked

It only fetches what it doesn't already know

Mount, refresh(), and a matching remote-change event fetch normally. But a key that merely joins the tracked list - useCloudCollection adding one right after a local write, say - is never re-fetched on that account alone. Without that guard, a slow fetch already in flight for the rest of the list could land a stale answer on top of a write that already resolved locally, the same bug useCloudItem's ticket exists to prevent, just triggered by list membership instead of a key change.

Loading only reflects the initial fetch and refresh()

A remote event that adds a key to the list, or names a key already tracked, updates values in the background without flipping loading - a live update from another device shouldn't flash a spinner over rows that are already showing something.

useCloudCollection

Adds the membership index useCloudItems needs but doesn't derive itself: every key starting with prefix, kept live.

function NotesList () {
const { keys, values, setValue, remove } = useCloudCollection<Note>(store, 'note:')

return keys.map(key => (
<NoteRow key={key} note={values[key]} onChange={next => setValue(key, next)} onDelete={() => remove(key)} />
))
}

Same shape as useCloudItems, plus keys: string[] for the current membership. Re-lists on mount and after any remote-change event that could plausibly have added or removed a member - not only one naming a key already under this prefix.

A setValue for a key not seen before joins keys immediately, so a newly created record appears in its own list without waiting on a round trip to confirm it. remove leaves the collection the same way.

useAccountStatus

import { icloudKV } from 'react-native-cloud-sync'
import { useAccountStatus } from 'react-native-cloud-sync/hooks'

function SyncBanner () {
const { status, identityChanged } = useAccountStatus(icloudKV)

if (status === 'noAccount') return <SignInPrompt />
if (identityChanged) return <SwitchedAccountNotice />
return null
}

All five AccountStatus values, not a boolean - "signed out", "temporarily unavailable" and "could not determine" each need different UI.

identityChanged is latched: it stays true once a different identity signs in, so a screen mounting right after the event still sees it.

usePendingWrites

Drives a "pending sync" indicator.

function PendingBadge () {
const { pending, flush, discard } = usePendingWrites(store)

if (pending.length === 0) return null

return (
<Row>
<Text>{pending.length} waiting to sync</Text>
<Button title="Retry now" onPress={flush} />
<Button title="Discard" onPress={() => discard()} />
</Row>
)
}

Polled every 2s by default, cheaply - pendingWrites() reads a small JSON blob synchronously. Pass 0 to poll only on mount.

Each entry carries lastErrorCode, so the indicator can say why it's stuck.

useRemoteChange

useRemoteChange(store, ({ keys }) => {
for (const key of keys) queryClient.invalidateQueries([key])
})

The listener is held in a ref, so an inline arrow function doesn't rebuild the subscription every render.

useQuota

const { quota, loading, refresh } = useQuota(store)

Not polled - a per-provider network round trip, moving too slowly to watch. Call refresh() on settings-screen open, or after a large write.

Cleaning up

None of these hooks call store.dispose() - a store outlives its components. Call it yourself when one is genuinely finished (sign-out, tearing down a test).