Offline sync
A persistent, connectivity-aware queue for mutations made while offline. Enqueue writes as they happen; they are stored locally and drained through your onSync handler when the device comes back online. On web, connectivity and persistence work out of the box; on React Native you inject them, keeping the same API on every platform.
1.Create the queue
Give the hook an onSync handler — typically the network write that was unavailable offline. It receives one queued payload at a time.
import { useOfflineSync } from '@zoharyandrianome/crosshooks';
function TodoComposer() {
const sync = useOfflineSync<{ title: string }>({
onSync: async (todo) => {
await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(todo),
});
},
});
// Render your UI here.
}Resolve from onSync to mark an item synced; throw (or reject) to keep it queued for a later retry.
2.Enqueue mutations
Call enqueue whenever the user performs a write. If the device is online, a flush is scheduled automatically; if it is offline, the item waits in the persisted queue.
<button onClick={() => sync.enqueue({ title: 'Buy milk' })}>
Add todo
</button>enqueue returns the created item, including the id you can later pass to remove.
3.Reflect connectivity and the pending queue
Read isOnline, pending, and isSyncing to keep your UI honest about unsynced work. All three are SSR-safe: the first render is optimistically online with an empty queue, then resolves after mount.
{!sync.isOnline && (
<p>You are offline. {sync.pending.length} change(s) will sync when you reconnect.</p>
)}
{sync.isSyncing && <Spinner />}4.Handle failures and retries
A flush processes items in enqueue order. When onSync throws, the item stays queued with a bumped attempts count and the pass stops — so a failed write never lets a later one jump ahead of it. The failure is exposed on error and cleared once the queue fully drains.
{sync.error && (
<button onClick={() => sync.flush()}>
Retry ({sync.pending.length}) — {sync.error.message}
</button>
)}A flush retries the whole queue from the head. Auto-flush on reconnect is on by default; pass autoFlushOnReconnect: false to drive it yourself.
5.Persistence on web
On web the queue is persisted to localStorage automatically, so it survives a reload or a closed tab. Set a storageKey to keep separate queues apart:
const sync = useOfflineSync({
onSync,
storageKey: 'todos:pending', // defaults to 'crosshooks:offline-sync'
});Writes are best-effort — a full or unavailable store (private mode, quota) degrades to an in-memory queue rather than throwing.
6.React Native setup
React Native has no localStorage or navigator.onLine, and crosshooks stays dependency-free, so you inject both. Pass storage to persist across app launches and connectivity to react to the network. Omit them and the queue lives in memory with the device assumed online — the return shape is identical to web, so shared components work unchanged.
import AsyncStorage from '@react-native-async-storage/async-storage';
import NetInfo from '@react-native-community/netinfo';
import type { ConnectivitySource } from '@zoharyandrianome/crosshooks';
const netInfo: ConnectivitySource = {
getSnapshot: () => lastKnownOnline, // seed from a NetInfo.fetch() at startup
subscribe: (onChange) =>
NetInfo.addEventListener((state) => onChange(state.isConnected ?? false)),
};
const sync = useOfflineSync({
onSync,
storage: AsyncStorage,
connectivity: netInfo,
});storage only has to match a small getItem / setItem / removeItem shape (sync or async), which AsyncStorage already satisfies.
Options
| Option | Type | Description |
|---|---|---|
| onSync | (payload, item) => unknown | Processes one queued item. Resolve to sync it; throw to retry later. |
| storageKey | string | Persistence key. Defaults to crosshooks:offline-sync. |
| storage | SyncStorage | Adapter. Defaults to localStorage on web; inject AsyncStorage on native. |
| connectivity | ConnectivitySource | Online/offline source. Defaults to navigator.onLine on web. |
| autoFlushOnReconnect | boolean | Flush automatically when connectivity returns. Defaults to true. |
Returns
| Field | Type | Description |
|---|---|---|
| isOnline | boolean | Whether the device is online. Optimistically true during SSR. |
| pending | SyncItem[] | Queued items awaiting sync, in enqueue order. |
| isSyncing | boolean | True while a flush pass is running. |
| error | Error | null | The most recent sync error, cleared once the queue drains. |
| enqueue | (payload) => SyncItem | Queue a payload; schedules a flush when online. |
| flush | () => Promise<SyncResult> | Drain the queue now. No-ops while offline or already syncing. |
| remove | (id) => void | Drop a single queued item without syncing it. |
| clear | () => void | Discard every queued item without syncing. |
Want to see it move? Open the live offline-sync demo — toggle offline, queue changes, and watch them drain on reconnect.