Back to Articles

A Master Guide to Offline Syncing in React Native & Expo

Step-by-step architectural pattern for implementing offline transaction queues, SQLite persistent storage, and auto-sync worker logic in Expo SDK 54.

A Master Guide to Offline Syncing in React Native & Expo

Building a mobile application that crashes or locks up when a user loses cellular connectivity is a recipe for high uninstalls. A premium mobile experience must be offline-first—storing actions locally and synchronizing them to the cloud once connectivity is restored.

Here is a step-by-step guide to implement a reliable, queue-based sync architecture using React Native, Expo, and SQLite.


1. The Offline Architecture Pattern

To support offline operations, our app does not send requests directly to our API. Instead, all data modifications go through a Local Queue Manager:

typescript
[User Action] ──> [Save to Local SQLite/Sync Queue] ──> [Update UI State] │ [NetInfo listens] │ ▼ (Is Network Connected?) / \ YES NO / \ [Process Queue & Push] [Wait in Queue]

2. Setting up the SQLite Queue Table

We can use expo-sqlite to keep a queue of synchronization operations. When offline, we write the request method, payload, and API endpoint to this table.

typescript
import * as SQLite from 'expo-sqlite'; const db = SQLite.openDatabaseSync('sync_queue.db'); export function initDatabase() { db.execSync(` CREATE TABLE IF NOT EXISTS pending_syncs ( id INTEGER PRIMARY KEY AUTOINCREMENT, endpoint TEXT NOT NULL, method TEXT NOT NULL, payload TEXT NOT NULL, createdAt TEXT NOT NULL ); `); }

3. Writing to Queue & Processing

When a user marks a checklist or submits a form, we verify connection status. If offline, we save the payload:

typescript
export function queueSyncRequest(endpoint: string, method: string, payload: any) { db.runSync( 'INSERT INTO pending_syncs (endpoint, method, payload, createdAt) VALUES (?, ?, ?, ?)', [endpoint, method, JSON.stringify(payload), new Date().toISOString()] ); }

When network connection is restored (detected via @react-native-community/netinfo), we process the queue sequentially:

typescript
export async function processSyncQueue() { const pending = db.getAllSyncs('SELECT * FROM pending_syncs ORDER BY id ASC'); for (const item of pending) { try { const response = await fetch(`https://api.myplatform.com${item.endpoint}`, { method: item.method, headers: { 'Content-Type': 'application/json' }, body: item.payload }); if (response.ok) { // Remove from local database queue upon successful post db.runSync('DELETE FROM pending_syncs WHERE id = ?', [item.id]); } } catch (error) { console.error('Sync failed for item:', item.id, error); break; // Stop and retry later if network fails again } } }

By queueing changes, the user experiences zero lag or modal blockages, and their data remains completely safe.