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_at and updated_at timestamps
  • A _status field (WatermelonDB manages this internally as created, updated, deleted)
  • A _changed field tracking which columns changed since last sync

WatermelonDB's SyncAdapter protocol expects your backend to expose two endpoints:

  1. /sync/pull — returns changes since a lastPulledAt timestamp
  2. /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.