A reliable offline-first sync vault for persistent network operations in React Native. Queue API requests offline, persist them in SQLite, and progressively sync when online. Failed requests are never lost.
NEW in v1.2.0+:
- β¨ Automatic API interception - intercepts
fetch()andaxios()calls automatically - β‘ Event-based reactive updates - hooks use event listeners instead of polling
- π§ Smart caching with intelligent invalidation
- π Resource-aware sync (battery & network quality)
- π Background sync guarantees
- π¨ Pre-built UI components
- π Sync metrics & dashboard
- π Basic integrations (React Query, Zustand, Redux) - Note: Basic adapters, manual integration required
πΉ Demo Video: Watch on YouTube
# Install
npm install react-native-sync-vault
# Setup (optional - automated setup tool)
npx sync-vault-initimport { useOfflineQueue } from 'react-native-sync-vault';
function MyComponent() {
const queue = useOfflineQueue({
baseUrl: 'https://api.example.com',
interceptor: { enabled: true }, // Automatically intercept fetch()
});
const handleSubmit = async () => {
// Just use fetch() - automatically queued if offline!
const response = await fetch('https://api.example.com/api/users', {
method: 'POST',
body: JSON.stringify({ name: 'John' }),
});
};
}- β A full-featured database with relationships (like WatermelonDB)
- β A general-purpose cache (like React Query cache)
- β An ORM or database abstraction layer
- β A state management library (like Redux or Zustand)
- β A polling-based system - uses event-driven architecture
- β A reliable network operation queue
- β Offline-first request persistence
- β Progressive sync engine
- β Failure persistence and retry logic
- β Custom native network monitoring (no external dependencies)
- β
Automatic API interception - Automatically intercepts
fetch()andaxios()calls and queues them when offline - β Event-based reactive updates - Hooks use event listeners for instant UI updates without polling
- β Smart caching with intelligent invalidation
- β Resource-aware sync (battery & network quality detection)
- β Offline Request Queue - Queue API requests when offline
- β
Automatic API Interception - Intercepts
fetch()calls automatically - β SQLite Persistence - Requests persisted in native SQLite
- β Progressive Sync - Automatic sync when network is restored
- β Conflict Resolution - Built-in conflict resolution strategies
- β Retry Logic - Automatic retry with exponential backoff
- β Native Network Monitoring - No external dependencies
- β Intelligent Cache - Per-endpoint TTL strategies
- β Pattern-Based Invalidation - Automatic cache invalidation on mutations
- β Stale-While-Revalidate - Return cached data while fetching fresh
- β Multiple Cache Strategies - Cache-first, network-first, stale-while-revalidate
- β Battery Monitoring - Reduces sync when battery is low
- β Network Quality Detection - Defers large uploads on cellular
- β Priority-Based Queue - Critical items sync first
- β Dynamic Batch Sizing - Adjusts based on available resources
- β Background Guarantees - Sync survives app kill
- β Task Scheduling - Platform-specific background tasks (iOS/Android)
- β Retry Policies - Configurable retry with exponential backoff
- β React Hooks - Easy-to-use React hooks
- β UI Components - Pre-built offline status indicators
- β
Offline-Ready Hooks - React Query-like API (
useOfflineQuery,useOfflineMutation) - β Sync Metrics - Track cache hit rates, sync success, and performance
- β Debug Tools - Built-in debug screen, logger, and dashboard
- β TypeScript - Full TypeScript support
β οΈ React Query - Basic adapter hook (manual integration required)β οΈ Zustand - Basic hook to add sync vault to store (manual usage)- β Redux - Middleware for queuing Redux actions with API metadata
- π API Reference - Complete API documentation
- π Migration Guide - Migrate from other solutions
- β‘ Performance Benchmarks - Performance metrics
- π οΈ Error Handling - Error handling guide
- π API Stability - API stability policy
- π Examples - Example applications
npm install react-native-sync-vault
# or
yarn add react-native-sync-vaultcd ios && pod installAdd network permission to AndroidManifest.xml:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />Note: The library uses autolinking, so no additional Gradle configuration is needed.
import { useOfflineQueue } from 'react-native-sync-vault';
function MyComponent() {
const queue = useOfflineQueue('https://api.example.com');
const handleSubmit = async () => {
try {
const requestId = await queue.push({
method: 'POST',
url: '/api/users',
data: { name: 'John Doe', email: 'john@example.com' },
priority: 1,
});
console.log('Request queued:', requestId);
} catch (error) {
console.error('Failed to queue request:', error);
}
};
return <Button onPress={handleSubmit} title="Submit" />;
}Enable automatic fetch() and axios() interception - no need to manually queue requests. Queued requests automatically trigger events for reactive UI updates.
import { useOfflineQueue } from 'react-native-sync-vault';
import axios from 'axios';
function MyComponent() {
// Enable automatic interception for both fetch() and axios()
const queue = useOfflineQueue({
baseUrl: 'https://api.example.com',
interceptor: {
enabled: true, // Enable fetch() interception
urlFilter: ['https://api.example.com'], // Optional: only intercept these URLs
axios: {
enabled: true, // Enable axios() interception
// instance: axios, // Optional: use custom axios instance
},
},
});
const handleSubmitWithFetch = async () => {
// Just use fetch() normally - it's automatically intercepted!
try {
const response = await fetch('https://api.example.com/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'John Doe',
email: 'john@example.com'
}),
});
if (response.status === 202) {
// Request was queued (device is offline)
// Events are automatically emitted: queue:request:added
console.log('Request queued for offline sync');
} else {
// Request executed normally (device is online)
const data = await response.json();
console.log('Request successful:', data);
}
} catch (error) {
console.error('Request failed:', error);
}
};
const handleSubmitWithAxios = async () => {
// Just use axios() normally - it's automatically intercepted!
try {
const response = await axios.post('https://api.example.com/api/users', {
name: 'John Doe',
email: 'john@example.com'
});
if (response.status === 202) {
// Request was queued (device is offline)
// Events are automatically emitted: queue:request:added
console.log('Request queued for offline sync');
} else {
// Request executed normally (device is online)
console.log('Request successful:', response.data);
}
} catch (error: any) {
// Check if it's a queued error
if (error.isQueued && error.status === 202) {
console.log('Request queued for offline sync', error.requestId);
} else {
console.error('Request failed:', error);
}
}
};
return (
<View>
<Button onPress={handleSubmitWithFetch} title="Submit with Fetch" />
<Button onPress={handleSubmitWithAxios} title="Submit with Axios" />
</View>
);
}Note: All hooks (useQueueStatus, usePendingRequests, useFailedRequests) use event listeners for reactive updates. When requests are queued via interception, events are automatically emitted and your UI updates immediately - no polling required!
All status hooks use event-based reactive updates instead of polling. When requests are queued (via fetch(), axios(), or manual queue.push()), events are automatically emitted and your UI updates immediately.
import { useQueueStatus, usePendingRequests, useFailedRequests } from 'react-native-sync-vault';
import axios from 'axios';
function QueueStatus() {
const { status, loading } = useQueueStatus();
const { pendingRequests } = usePendingRequests();
const { failedRequests, retry } = useFailedRequests();
// Example: When axios request is queued offline, events trigger automatically:
// - queue:request:added event is emitted
// - usePendingRequests hook updates reactively
// - useQueueStatus hook updates reactively
// No polling or manual refresh needed!
const handleAxiosRequest = async () => {
try {
await axios.post('https://api.example.com/api/data', { name: 'Test' });
} catch (error: any) {
if (error.isQueued) {
// Request was queued - UI will update automatically via events
console.log('Request queued, UI will update automatically');
}
}
};
return (
<View>
<Text>Network: {status?.networkStatus}</Text>
<Text>Pending: {status?.totalPending}</Text>
<Text>Failed: {status?.totalFailed}</Text>
{/* These lists update automatically when events are emitted */}
{pendingRequests.map((request) => (
<View key={request.id}>
<Text>{request.method} {request.url}</Text>
</View>
))}
{failedRequests.map((request) => (
<View key={request.id}>
<Text>{request.errorMessage}</Text>
<Button onPress={() => retry(request.requestId)} title="Retry" />
</View>
))}
</View>
);
}Event-Based Updates: Hooks subscribe to events like queue:request:added, queue:request:synced, queue:pending:changed, and sync:started for instant, reactive updates without polling.
The sync vault uses an event-based architecture for reactive updates. When requests are queued (via fetch(), axios(), or queue.push()), events are automatically emitted. All hooks subscribe to these events for instant UI updates.
Available Events:
queue:request:added- Emitted when a request is added to the queuequeue:request:synced- Emitted when a request is successfully syncedqueue:request:failed- Emitted when a request fails to syncqueue:status:changed- Emitted when request status changesqueue:pending:changed- Emitted when pending requests list changesqueue:failed:changed- Emitted when failed requests list changessync:started- Emitted when sync process startssync:completed- Emitted when sync process completessync:failed- Emitted when sync process failssync:batch:completed- Emitted when a sync batch completes
Listening to Events Directly:
import QueueManager from 'react-native-sync-vault';
// Listen to events
QueueManager.on('queue:request:added', ({ requestId, request }) => {
console.log('Request queued:', requestId, request.method, request.url);
});
QueueManager.on('queue:request:synced', ({ requestId }) => {
console.log('Request synced:', requestId);
});
QueueManager.on('sync:started', () => {
console.log('Sync started');
});
QueueManager.on('sync:completed', () => {
console.log('Sync completed');
});
// Clean up listeners
QueueManager.off('queue:request:added', handler);Note: Hooks automatically subscribe to these events, so you typically don't need to listen directly unless you're building custom integrations.
Enable intelligent caching with per-endpoint strategies:
import { useOfflineQueue } from 'react-native-sync-vault';
function MyComponent() {
const queue = useOfflineQueue({
baseUrl: 'https://api.example.com',
cache: {
enabled: true,
defaultTtl: 8 * 60 * 60, // 8 hours
strategies: {
'/api/assignments': {
ttl: 24 * 60 * 60, // 24h for critical data
staleWhileRevalidate: true
},
},
invalidateOnMutate: {
'POST /api/assignments': ['/api/assignments', '/api/assignments/*'],
}
}
});
// GET requests are automatically cached
const response = await fetch('https://api.example.com/api/assignments');
}Optimize sync based on device resources:
import { useOfflineQueue } from 'react-native-sync-vault';
function MyComponent() {
const queue = useOfflineQueue({
baseUrl: 'https://api.example.com',
resourceAware: {
battery: {
lowThreshold: 20,
action: 'defer_non_critical',
criticalOnly: true
},
network: {
qualityAware: true,
deferLargeUploadsOnCellular: true,
maxPayloadSizeOnCellular: 100 * 1024 // 100KB
}
}
});
}Ensure critical operations complete even after app kill:
import { useBackgroundSync } from 'react-native-sync-vault';
function MyComponent() {
const { guarantee } = useBackgroundSync();
const handleCriticalSubmit = async () => {
await guarantee({
taskId: 'submit-inspection-123',
operation: () => queue.sync(),
requiredNetwork: true,
priority: 'high',
retryPolicy: {
maxAttempts: 3,
backoff: 'exponential'
}
});
};
}Use familiar React Query patterns:
import { useOfflineQuery, useOfflineMutation } from 'react-native-sync-vault';
function MyComponent() {
const { data, isLoading, isStale } = useOfflineQuery('/api/assignments', {
ttl: 8 * 60 * 60 * 1000,
staleWhileRevalidate: true
});
const { mutate, isPending } = useOfflineMutation({
fn: (data) => fetch('/api/assignments', {
method: 'POST',
body: JSON.stringify(data)
}),
invalidateOnSuccess: ['/api/assignments']
});
return (
<View>
{isLoading && <Text>Loading...</Text>}
{data && <Text>{JSON.stringify(data)}</Text>}
<Button onPress={() => mutate({ title: 'New Assignment' })} title="Create" />
</View>
);
}Add pre-built UI components for offline status:
import {
OfflineProvider,
OfflineStatusBar,
QueuedActionsBadge,
SyncProgress
} from 'react-native-sync-vault';
function App() {
return (
<OfflineProvider config={{ baseUrl: 'https://api.example.com' }}>
<OfflineStatusBar position="top" />
<YourAppContent />
<QueuedActionsBadge />
<SyncProgress />
</OfflineProvider>
);
}Track sync performance and health:
import { useSyncMetrics } from 'react-native-sync-vault';
function MetricsDashboard() {
const { metrics } = useSyncMetrics();
return (
<View>
<Text>Queue Size: {metrics.queueSize}</Text>
<Text>Cache Hit Rate: {metrics.cacheHitRate}%</Text>
<Text>Sync Success Rate: {metrics.syncSuccessRate}%</Text>
<Text>Average Sync Time: {metrics.averageSyncTime}ms</Text>
</View>
);
}Status: Basic adapter - returns separate queryClient and queue objects. Manual integration required.
The useReactQueryWithSyncVault hook returns both a QueryClient and queue instance, but they are not automatically connected. You'll need to manually integrate them:
import { ReactQueryIntegration } from 'react-native-sync-vault/integrations';
import { useMutation, useQuery } from '@tanstack/react-query';
function MyComponent() {
const { queryClient, queue } = ReactQueryIntegration.useReactQueryWithSyncVault({
baseUrl: 'https://api.example.com'
});
// Manual integration example:
// You need to wrap your mutations to use the queue
const mutation = useMutation({
mutationFn: async (data) => {
// Check if offline and queue if needed
if (!queue.networkMonitor?.isOnline()) {
await queue.push({
method: 'POST',
url: '/api/todos',
data
});
return { queued: true };
}
// Otherwise make normal API call
return fetch('https://api.example.com/api/todos', {
method: 'POST',
body: JSON.stringify(data)
}).then(r => r.json());
}
});
// Note: Requires @tanstack/react-query to be installed
// Note: The withSyncVault function is non-functional (only stores config)
}Limitations:
- No automatic mutation queuing
- No cache integration between React Query and sync vault
- Manual error handling required
Status: Basic hook - adds sync vault to store state. Manual usage required.
withSyncVault function is broken (violates React hooks rules) and should not be used. Only use useZustandWithSyncVault.
import { create } from 'zustand';
import { ZustandIntegration } from 'react-native-sync-vault/integrations';
// Create your Zustand store
const useStore = create((set) => ({
todos: [],
addTodo: (todo) => set((state) => ({ todos: [...state.todos, todo] }))
}));
function MyComponent() {
// Use the integration hook to get store state + syncVault
const store = ZustandIntegration.useZustandWithSyncVault(useStore, {
baseUrl: 'https://api.example.com'
});
// Access your store state plus syncVault
// You'll need to manually use queue.push() for API calls
const handleAddTodo = async (todo) => {
store.addTodo(todo);
// Manually queue API call
await store.syncVault.queue.push({
method: 'POST',
url: '/api/todos',
data: todo
});
};
return (
<View>
{store.todos.map(todo => <Text key={todo.id}>{todo.text}</Text>)}
<Button onPress={() => handleAddTodo({ id: 1, text: 'New' })} title="Add" />
<Button onPress={() => store.syncVault.sync()} title="Sync" />
</View>
);
}Limitations:
- No automatic API call queuing from store actions
- Manual queue management required
- The
withSyncVaultwrapper function is broken and should not be used
Status: Functional - Basic middleware for queuing actions with API metadata.
This is the most functional integration. It automatically queues Redux actions that include API call metadata:
import { createStore, applyMiddleware } from 'redux';
import { ReduxIntegration } from 'react-native-sync-vault/integrations';
const middleware = [
ReduxIntegration.createSyncVaultMiddleware({
baseUrl: 'https://api.example.com'
})
];
const store = createStore(reducer, applyMiddleware(...middleware));
// Actions with API call metadata will be queued when offline:
dispatch({
type: 'CREATE_TODO',
payload: { text: 'New todo' },
meta: {
apiCall: {
method: 'POST',
url: '/api/todos',
data: { text: 'New todo' }
},
queueIfOffline: true
}
});What it does:
- Automatically queues actions with
meta.apiCallandmeta.queueIfOffline: true - Works when device is offline
Limitations:
- Basic implementation - no automatic sync retry integration
- No automatic success/failure action dispatching
- Requires manual action metadata structure
See docs/API_REFERENCE.md for complete API documentation.
Queue Management:
useOfflineQueue(config?)- Main hook for queue managementuseQueueStatus()- Monitor queue status (event-based reactive updates)usePendingRequests(limit?)- Get pending requests (event-based reactive updates)useFailedRequests()- Get failed requests with retry (event-based reactive updates)
Caching:
useCache(cacheManager?)- Access cache operationsuseSmartCache(config?)- Smart cache with invalidation
Offline-Ready:
useOfflineQuery(key, options?)- React Query-like query hookuseOfflineMutation(options?)- React Query-like mutation hook
Resource & Background:
useResourceAwareSync(config?)- Resource-aware sync configurationuseBackgroundSync()- Background sync guarantees
Metrics:
useSyncMetrics()- Sync performance metrics
queue.push(options)- Queue a requestqueue.getPendingRequests(limit?, offset?)- Get pending requestsqueue.getFailedRequests()- Get failed requestsqueue.sync()- Manually trigger syncqueue.getCacheManager()- Get cache manager instance
OfflineProvider- Context provider for offline stateOfflineStatusBar- Connection status indicatorQueuedActionsBadge- Pending actions counterSyncProgress- Sync progress indicator
DebugScreen- Built-in debug screenQueueLogger- Queue logging utilitySyncDashboard- Sync metrics dashboard
ReactQueryIntegration.useReactQueryWithSyncVault(config)- Returns{ queryClient, queue }(manual integration required)ZustandIntegration.useZustandWithSyncVault(store, config)- Adds sync vault to Zustand store state (manual usage required)ReduxIntegration.createSyncVaultMiddleware(config)- Middleware for queuing Redux actions (functional, basic implementation)ReduxIntegration.withSyncVault(reducer, config)- Helper to set up Redux with sync vault middleware
Note: ZustandIntegration.withSyncVault is broken and should not be used. ReactQueryIntegration.withSyncVault is non-functional (only stores config).
Check out the examples directory for complete example applications:
- Todo App - Complete offline-first todo management with automatic interception, caching, and error handling
- Initialization: < 50ms
- Enqueue time: < 5ms
- Memory overhead: < 1KB per request
- Batch sync: < 100ms per batch of 5 requests
- Cache lookup: < 2ms
- Cache hit rate: 60-80% in typical usage
See Performance Benchmarks for detailed metrics and comparisons.
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
MIT
- π Documentation
- π Report Issues
- π¬ Discussions
- π§ Email: punithm300@gmail.com
Made with β€οΈ for the React Native community