Connectivity is never guaranteed. Warehouses, hospitals, construction sites, parking garages, and rural areas all create dead zones that break apps designed to assume constant internet access. Offline-first architecture isn't just a feature for niche use cases — it's a reliability standard that separates professional mobile apps from brittle ones. This guide covers the core strategies for building offline-capable React Native applications with clean sync behavior.
The Offline-First Mindset
Most apps are built "online-first" — they hit an API, display results, and fail gracefully (if at all) when the network drops. Offline-first inverts this: the app reads from and writes to local storage first, and the network is treated as a sync mechanism rather than a dependency.
This shift requires rethinking your data layer. Instead of asking "what happens when the API fails?", you ask "how does local state sync with the server when connectivity is available?"
For more context on mobile architecture, see our guide on React Native development best practices.
Local Storage Options in React Native
AsyncStorage
The built-in key-value store. Good for small amounts of structured data — user preferences, auth tokens, simple application state. Not suitable for complex queries or large datasets. Performance degrades with large values.
WatermelonDB
A high-performance SQLite-based database for React Native with built-in sync protocol support. Designed explicitly for offline-first apps with large datasets. Uses lazy loading, works well with thousands of records, and has official adapters for Supabase and Firebase. The best choice for complex data models.
MMKV
An extremely fast key-value store (10x faster than AsyncStorage for synchronous operations). Ideal for frequently accessed small data — settings, cached API responses, session state. Uses memory-mapped files for near-instant reads.
SQLite via expo-sqlite or react-native-sqlite-storage
Full SQL capabilities for complex queries. More setup than WatermelonDB but maximum flexibility. Good choice when you need full SQL expressiveness and aren't using WatermelonDB's sync protocol.
Detecting Network State
The foundation of any offline strategy is reliably knowing when you're connected:
import NetInfo from '@react-native-community/netinfo';
const useNetworkStatus = () => {
const [isConnected, setIsConnected] = useState(true);
useEffect(() => {
const unsubscribe = NetInfo.addEventListener(state => {
setIsConnected(state.isConnected && state.isInternetReachable);
});
return unsubscribe;
}, []);
return isConnected;
};
Note: isConnected alone isn't sufficient — a device can be connected to WiFi with no internet access. Always check isInternetReachable for accurate offline detection.
Optimistic Updates: The Core Pattern
Optimistic updates are the foundation of a snappy offline experience. When a user takes an action (create, update, delete), you immediately update local state and UI — then sync to the server in the background.
The Basic Flow
- User triggers an action (e.g., marks a task complete)
- App immediately updates local state and persists to SQLite/WatermelonDB
- UI reflects the change instantly — no loading spinner
- App queues a network request to sync the change
- If online: request fires, server confirms, local record updates with server ID/timestamp
- If offline: request stays queued until connectivity returns, then syncs
Building a Sync Queue
The sync queue is a local log of pending operations that haven't yet reached the server:
// Queue structure
{
id: 'local-uuid',
operation: 'CREATE' | 'UPDATE' | 'DELETE',
table: 'tasks',
payload: { ... },
createdAt: timestamp,
retryCount: 0,
lastError: null
}
When connectivity returns, process the queue in order. For each item:
- Attempt the API call
- On success: remove from queue, update local record with server response
- On failure: increment retry count, apply exponential backoff, flag for review after N retries
Conflict Resolution Strategies
When multiple clients modify the same record offline, conflicts are inevitable. Common resolution strategies:
Last Write Wins (LWW)
The operation with the latest timestamp wins. Simple to implement, but can silently discard changes. Appropriate for data where the latest value is always correct (e.g., location, status).
Server Wins
On conflict, the server's version is authoritative. Local changes are discarded. Safe for system-managed fields like computed totals or server-assigned IDs.
Client Wins
Local changes always override server state on sync. Dangerous for shared data but appropriate for user-specific preferences.
Merge / CRDT
Conflict-free Replicated Data Types allow concurrent edits to be merged without conflicts. Complex to implement but the gold standard for collaborative applications. Libraries like Automerge and Yjs implement CRDTs for JavaScript.
Practical Implementation with Supabase
Supabase's Realtime and REST APIs work well with offline sync patterns. A typical setup:
- WatermelonDB as local store with SQLite adapter
- Custom sync function that compares
updated_attimestamps - Pull changes:
GET /table?updated_at=gt.{lastSyncedAt} - Push changes: process the local sync queue via REST
- Realtime subscription for live updates when online
Testing Offline Behavior
Offline testing is often overlooked. Build it into your development workflow:
- iOS Simulator: Network Link Conditioner in Additional Tools for Xcode allows simulating packet loss, latency, and complete disconnection
- Android Emulator: Use extended controls to simulate network conditions
- Physical device testing: Enable Airplane Mode mid-operation to test real disconnection handling
- Unit tests: Mock NetInfo to test both connected and disconnected code paths
Frequently Asked Questions
How do I handle authentication tokens offline?
Store tokens in MMKV or SecureStore (for sensitive data). Allow the app to function with a cached token when offline. Refresh the token when connectivity returns, before the next sync cycle. Log out users only if the token expires while offline and can't be refreshed on reconnect.
Should I cache API responses or use a local database?
For read-heavy data that doesn't change often, response caching (React Query's cacheTime, for example) is often sufficient. For write-heavy data or data the user needs to modify offline, a local database with sync is the right choice.
How do I handle large media files offline?
Don't store large files in SQLite. Use a file system cache (react-native-fs) for images and documents. Queue uploads separately from data sync. Show pending upload indicators in the UI so users know their content is staged.
What's the performance impact of WatermelonDB vs AsyncStorage?
For datasets under 100 records, the difference is negligible. For 1,000+ records with frequent reads/writes, WatermelonDB is dramatically faster due to its lazy loading and native SQLite execution. If your app will grow, start with WatermelonDB from day one.
Related Reading
- Mobile Development: The Complete Guide
- Biometric Authentication for Mobile Apps
- Mobile Payments Integration Guide
- Wearable App Development Guide
Building a mobile app that needs to work offline?
We design and build React Native applications with offline-first architecture — from data model to sync protocol to production deployment.
Let's Build Your Mobile App