Building Offline-First React Native Apps: Architecture and Sync Patterns
How to architect an offline-first React Native app — local database selection, sync strategy, conflict resolution, and UI patterns that work without network.
Most mobile apps assume a reliable network connection and break gracefully — or not so gracefully — when that assumption fails. An offline-first app inverts the assumption: the app works fully from local data, and network connectivity is treated as an enhancement rather than a requirement.
Building offline-first correctly is a meaningful engineering investment. This guide covers the architecture decisions that make the difference between a working offline experience and one that corrupts data or confuses users.
When Offline-First is Worth Building
Not every app needs offline-first architecture. It is worth the investment when:
- Users are in environments with unreliable connectivity (field work, travel, rural areas, underground infrastructure)
- App interactions need to feel instant regardless of network latency (note-taking, task management, forms)
- Data loss on network failure is unacceptable (orders, inspections, medical records)
- Your app competes with tools that already work offline (productivity apps, document editors)
If your app is primarily a display surface for remote data and users have reliable connectivity, full offline support may not be worth the complexity. A well-implemented caching layer with clear error states may be sufficient.
The Core Architecture for React Native Offline-First Apps
The model is straightforward:
- Local database is the source of truth for the UI at all times
- Reads always come from local data — never wait for the network to render a screen
- Writes go to the local database immediately — the UI reflects the change at once
- Sync moves local changes to the server and remote changes to the device, running in the background
This means the UI is fast and predictable by design. Network operations are a background concern, not a blocking concern.
Choosing a Local Database for React Native
WatermelonDB
WatermelonDB is the standard choice for React Native offline-first applications with relational data. Its core design choices align with the offline-first model:
- Lazy loading: data is only loaded when observed, which keeps UI performance high as the local dataset grows
- Reactive queries: components subscribe to database queries and update automatically when relevant records change
- Built-in sync protocol: WatermelonDB defines a sync protocol spec that your backend can implement
The main cost is the setup complexity. WatermelonDB requires understanding its Model system and the JSI-based native SQLite bindings. It is not a simple key-value store.
MMKV
MMKV is a fast, synchronous key-value storage library for React Native, implemented with memory-mapped files. It is appropriate for:
- User preferences and settings
- Auth tokens and session data
- Small, frequently-accessed values that do not require relational queries
MMKV is not a replacement for WatermelonDB for structured relational data. Use both in the same app for different concerns.
AsyncStorage
AsyncStorage is the basic key-value store that ships with React Native. It is adequate for simple values but is too slow and unstructured for any significant offline data model. If you are building offline-first, use MMKV instead of AsyncStorage.
Sync Architecture: Push, Pull, and Conflict Resolution
The sync problem has three parts: pushing local changes to the server, pulling remote changes to the device, and resolving conflicts when both sides changed the same record.
Push: Queuing Local Writes
When a user creates, updates, or deletes a record while offline, the change is written to the local database and added to a sync queue. The sync queue is itself persisted locally — it must survive app restarts.
When the network is available, a background sync process reads the queue and sends changes to the server in order. Each queued operation should be idempotent — if the sync request is sent twice (due to a retry after a timeout), the server should produce the same result.
Assign a client-generated UUID to every record at creation time. Do not wait for the server to assign an ID. This eliminates the need to resolve a "pending" state while waiting for the server to acknowledge a new record.
Pull: Receiving Remote Changes
The standard pull model uses a server-side timestamp or sequence number. The device tracks the last successfully synced timestamp. On sync, it requests all records changed after that timestamp.
The server returns a change set — a list of created, updated, and deleted records. The device applies these to the local database.
WatermelonDB's sync protocol formalizes this exact model. If you implement a compatible server endpoint, the client sync process is handled for you.
Conflict Resolution
Conflicts occur when the same record was changed locally and remotely between syncs. The strategies:
- Last write wins: the record with the later
updated_attimestamp overwrites the other. Simple to implement, loses data when both edits are valid. - Field-level merging: each field is updated independently, with the later timestamp winning per field. Preserves more user intent.
- User-presented conflicts: show the user both versions and let them choose. Only appropriate when both versions have meaningful differences the user cares about.
For most business applications, last-write-wins with a timestamp is sufficient and predictable. Document or collaborative applications where multiple users edit the same record concurrently need a more sophisticated strategy (CRDTs or operational transforms).
UI Patterns for Offline-First Apps
Optimistic updates: write to local state immediately and show the result in the UI. Sync in the background. If the sync fails permanently (server error, validation failure), surface the error and offer a retry or undo — do not silently discard the change.
Sync status indicators: show users a clear indication of sync state. A subtle indicator of "syncing," "synced," or "changes pending" gives users confidence that their data is not lost. The pattern used by Apple Notes (a sync icon in the corner) is the baseline expectation for offline-capable apps.
Network-aware UI: do not show empty loading states for data that is available locally. Render local data immediately, then update when the network refresh completes. Use a subtle "last updated X minutes ago" label rather than a blocking spinner.
Graceful degradation for server-only features: some features require a server response by nature (payment processing, sending an email). Handle these with a clear message when offline rather than allowing the user to submit an action that cannot complete.
Testing Offline Behavior
Testing offline logic is often neglected until users report bugs. Recommended test practices:
- Write unit tests for sync queue processing and conflict resolution logic — these are pure functions and straightforward to test
- Test the sync protocol against a local server instance in integration tests
- Use the iOS Simulator's network conditioning to test the app under varying network quality
- Test the specific scenario of: create record offline, go online, sync, kill app, reopen — and verify data integrity
Offline-first is an architecture commitment, not a feature you add at the end. If you are scoping a product that needs offline capability, Clixo builds React Native apps with the data architecture to support it from day one.