@krishnavm/react-native-offline-sync
v2.0.1
Published
An offline-first data synchronization library for React Native & Expo.
Downloads
493
Maintainers
Readme
@krishnavm/react-native-offline-sync 📡
An offline-first data synchronization library for React Native & Expo. Automatically caches your data mutations when the device goes offline using any storage adapter and syncs them back to your server with configurable conflict resolution (Last-Write-Wins or per-field merge) when the network is restored.
⚡ Core Features (v2)
| Feature | Details |
|---|---|
| 🔄 Backend Agnostic | Provide your own pushToRemote and pullFromRemote callbacks. Use with REST, Supabase, Firebase, GraphQL, etc. |
| 🚀 Storage Agnostic | Bring your own local storage (e.g. SQLite, MMKV, WatermelonDB). |
| 🔀 Conflict Resolution | Last-Write-Wins (default) or true per-field merge when fieldTimestamps are provided. Manual mode for custom UI resolution. |
| 📡 Network Lifecycle | Uses @react-native-community/netinfo with reachability probing and AppState foreground/background awareness. |
| 🚄 Priority Queue | Debounced auto-sync, critical manual sync, and exponential backoff with jitter for failed retries. |
| 🔑 Idempotency | Auto-generated idempotency keys on each record to prevent duplicate writes during retries. |
| 📄 Paginated Pull | Optional pullFromRemotePaginated to chunk large result sets and avoid OOM on low-end devices. |
| 🔌 Pluggable Logger | Replace the default logger with your own (e.g. Sentry, Crashlytics) via setLogger(). |
📦 Installation
npm install @krishnavm/react-native-offline-sync @react-native-community/netinfo(Note: Since this library relies on the @react-native-community/netinfo native module, Expo users will need to create a development build).
🚀 Quick Start (v2)
1. Define your Entity Sync Config
The engine is built around typed entities. Define how to read/write locally, and how to push/pull remotely.
import { EntitySyncConfig, SyncableRecord } from '@krishnavm/react-native-offline-sync';
interface Todo extends SyncableRecord {
title: string;
completed: boolean;
}
const todoConfig: EntitySyncConfig<Todo> = {
entityName: 'todos',
// Local Database Operations (e.g. SQLite, MMKV)
getPendingRecords: async () => localDb.getPendingTodos(),
getLocalRecord: async (id) => localDb.getTodo(id),
insertLocalRecord: async (data) => localDb.insertTodo(data),
updateLocalRecord: async (id, data) => localDb.updateTodo(id, data),
// Remote API Operations (e.g. REST, Supabase, Firebase)
pushToRemote: async (records) => {
const success = [];
const failed = [];
for (const record of records) {
try {
await api.post('/todos', record);
success.push(record.id);
} catch {
failed.push(record.id);
}
}
return { success, failed };
},
pullFromRemote: async (since, userId) => {
return await api.get(`/todos?updatedAfter=${since.toISOString()}`);
}
};2. Initialize the Sync Engine & Provider
import { SyncEngine, SyncProvider } from '@krishnavm/react-native-offline-sync';
const syncEngine = new SyncEngine([todoConfig]);
export default function App() {
return (
<SyncProvider engine={syncEngine} userId="user-123">
<YourApp />
</SyncProvider>
);
}3. Use the React Hooks
import { useSyncEngine, useSyncStatus, useIsConnected } from '@krishnavm/react-native-offline-sync';
import { View, Text, Button } from 'react-native';
export function SyncStatusWidget() {
const isConnected = useIsConnected();
const { isSyncing } = useSyncStatus();
const { triggerSync } = useSyncEngine();
return (
<View>
<Text>Network: {isConnected ? 'Online' : 'Offline'}</Text>
<Text>Status: {isSyncing ? 'Syncing...' : 'Idle'}</Text>
<Button title="Force Sync" onPress={() => triggerSync('manual')} />
</View>
);
}🤝 Migrating from v1
⚠️ Breaking change in v2: The
useOfflineSynchook is deprecated and will throw ifaddMutation()is called. It exists only as a migration placeholder.
Migrate to the v2 EntitySyncConfig + SyncEngine pattern:
// Before (v1) — NO LONGER WORKS
// const { addMutation } = useOfflineSync(syncFn, { storage });
// After (v2)
const todoConfig: EntitySyncConfig<Todo> = {
entityName: 'todos',
getPendingRecords: async () => localDb.getPendingTodos(),
// ... see Quick Start above
};
const engine = new SyncEngine([todoConfig]);🔀 Conflict Resolution
The default strategy is Last-Write-Wins (LWW) using the record's updatedAt timestamp.
For true per-field merge, populate fieldTimestamps on your records:
const record: SyncableRecord = {
id: '1',
title: 'Updated title',
description: 'Original description',
updatedAt: '2024-06-01T00:00:00Z',
fieldTimestamps: {
title: '2024-06-01T00:00:00Z',
description: '2024-01-01T00:00:00Z',
},
};When both local and remote records have fieldTimestamps, conflicts are resolved per-field. Without fieldTimestamps, the 'field-level' strategy degrades to record-level LWW.
🔄 Retry & Backoff
Failed push attempts use exponential backoff with jitter. Configure via retryConfig:
const config: EntitySyncConfig<Todo> = {
// ...
retryConfig: {
maxRetries: 5, // default: 5
baseDelayMs: 1000, // default: 1s
maxDelayMs: 300000, // default: 5 min
},
};🔌 Custom Logger
Replace the built-in logger with your own:
import { setLogger } from '@krishnavm/react-native-offline-sync';
setLogger({
debug: (msg, ...args) => myLogger.debug(msg, ...args),
info: (msg, ...args) => myLogger.info(msg, ...args),
warn: (msg, ...args) => myLogger.warn(msg, ...args),
error: (msg, ...args) => myLogger.error(msg, ...args),
});📄 License
MIT © Krishna
