How to Build an Offline-First Mobile App with React Native
Connectivity in Accra at 8 AM during rush hour is not the same as connectivity in a San Francisco office. Networks drop. Data is expensive. Users switch between Wi-Fi and mobile data mid-session. If your React Native app assumes a stable internet connection, you are not building for the real world — at least not for most of it.
Offline-first is not a feature. It is an architectural decision you make before you write a single component. This guide walks through the concrete patterns and tools that make it work, with a focus on WatermelonDB, sync strategies, and conflict resolution.
Why "Handle No Internet" Is the Wrong Frame
Most developers treat offline support as an edge case — show a banner, block the UI, retry the request. That is offline-tolerant, not offline-first.
Offline-first means the app works fully from local data and syncs opportunistically when a connection is available. The user never waits on the network for reads. Writes are queued locally and flushed later. The network becomes an implementation detail, not a dependency.
This is especially critical in markets where:
- Mobile data costs make users toggle connectivity deliberately
- Tower congestion causes frequent micro-dropouts
- Users operate across 2G, 3G, and 4G within the same commute
Choosing WatermelonDB Over AsyncStorage
AsyncStorage is fine for small key-value persistence — auth tokens, user preferences. It is not a database. Once you need relational data, queries, or observable record sets, you need something better.
WatermelonDB is a high-performance reactive database for React Native built on SQLite. Its key design principles align perfectly with offline-first:
- Lazy loading — only observed records trigger re-renders
- Reactive queries — UI automatically updates when local data changes
- Built-in sync protocol — a first-class sync adapter interface
- Schema migrations — safe to evolve your data model across app versions
Install it alongside the SQLite adapter:
npm install @nozbe/watermelondb
npm install @nozbe/react-native-sqlite-adapter
# iOS
cd ios && pod install
# Android — no extra steps needed for most setups
Modeling Your Data for Sync
Before writing sync logic, model your data correctly. Every record that will sync needs:
- A stable
id(UUID, generated on the client) created_atandupdated_attimestamps- A
_statusfield (WatermelonDB manages this internally ascreated,updated,deleted) - A
_changedfield tracking which columns changed since last sync
WatermelonDB's SyncAdapter protocol expects your backend to expose two endpoints:
/sync/pull— returns changes since alastPulledAttimestamp/sync/push— accepts local changes made since the last sync
This pull-then-push pattern is intentional. You always fetch the server's latest state first, resolve conflicts locally, then push your changes. It avoids a class of race conditions where a blind push overwrites a concurrent server update.
Sync Strategies: When and How Often
Sync frequency depends on your use case, but a few patterns work well in low-bandwidth environments:
Trigger-based Sync
Sync when the user explicitly saves, or when the app comes to the foreground after a background period. This conserves data and battery. Suitable for forms, field data collection, and CRM-style apps.
Delta Sync
Only transfer records changed since lastPulledAt. WatermelonDB's built-in sync protocol does this natively. Never pull the entire dataset on every sync — it kills low-bandwidth users.
Batched Pushes
Queue writes locally and push in a single batched request. Fewer round trips means more reliability on spotty connections. Group changes by table and send them together.
Background Sync with Exponential Backoff
For apps that need near-real-time consistency, use a background sync loop with exponential backoff on failure:
- Attempt sync every 30 seconds
- On failure, wait 1 min → 2 min → 4 min → cap at 15 min
- Reset the timer on success or when the user re-enters the app
Conflict Resolution: The Hard Part
Conflicts happen when the same record is modified on two devices (or by two users) between syncs. There is no magic — you need a defined resolution strategy.
Last-Write-Wins (LWW)
The simplest approach. Compare updated_at timestamps; the newer one wins. It is lossy — one update is silently discarded — but it is acceptable for many single-user apps.
Field-Level Merging
WatermelonDB's _changed field tells you exactly which columns were modified locally. If the server changed status and the client changed notes, merge both. Only trigger a conflict when the same field was changed on both sides.
Server-Authoritative Resolution
For sensitive data — inventory counts, financial records — let the server always win on conflict, but preserve the client's version in a pending_review state. Surface it to the user for manual resolution.
Choose your strategy per record type. A user profile can use LWW. An inventory record needs server authority. A chat message should never be overwritten — append only.
Handling Network Detection Responsibly
NetInfo from @react-native-community/netinfo tells you the connection type and reachability. But reachability can lie — a device can be "connected" to Wi-Fi with no actual data path.
A more reliable pattern is to attempt a lightweight heartbeat request to your own API (a /ping endpoint returning 200 with minimal payload) and treat the response as the true connectivity signal. Trigger sync only when this succeeds.
What Your Backend Needs to Support
Offline-first is a full-stack concern. Your API must:
- Accept and return timestamps in UTC ISO 8601 format consistently
- Support idempotent writes (re-sending the same client-generated UUID should not create duplicates)
- Return soft-deleted records in pull responses (so clients can remove them locally)
- Handle out-of-order pushes gracefully — a record created on day 1 might arrive on day 3
If your backend is not built for idempotency and delta pulls, bolting on a sync layer will produce subtle data corruption bugs that are extremely hard to trace.
Why This Matters for Your Project
If you are building a mobile product for African markets — or any market where connectivity is variable — offline-first architecture is not a nice-to-have. It is what separates apps that get uninstalled from apps that become daily utilities. WatermelonDB gives you a production-grade local data layer and a sync protocol that maps cleanly onto a RESTful or GraphQL backend. The investment in modeling your data correctly upfront pays dividends every time a user's network drops and your app keeps working without complaint. At Code!nk Technologies, this is the baseline we build from — because the networks our users live on demand nothing less.





