In today’s mobile-first world, ensuring your app works seamlessly without an internet connection is no longer a luxury — it’s a necessity. This is where the Offline-First architecture comes in. In this guide, we’ll implement an offline-first architecture in a React Native application using local databases, data synchronization, and network status handling.
Table of Contents
- What is Offline-First Architecture?
- Why Offline-First in React Native?
- Tools and Libraries
- Setting up the Project
- Using SQLite with react-native-sqlite-storage
- Detecting Network Status
- Syncing Data with Remote API
- Handling Conflicts
- Example Use Case: Notes App
- Best Practices
- Conclusion
What is Offline-First Architecture?
Offline-First means your application prioritizes local data and uses the network only when available. The user should be able to read/write data offline, and it should sync with the backend once the network is available.
Why Offline-First in React Native?
- Better UX in unstable networks.
- Faster access to data.
- Essential for travel, rural areas, or low-end devices.
Tools and Libraries
We’ll use:
- react-native-sqlite-storage: Local database storage
- @react-native-community/netinfo: Network status
- axios: API requests
- redux or zustand: For state management (optional)
- background-fetch or custom logic for sync (optional for auto-sync)
Install dependencies:
npm install react-native-sqlite-storage @react-native-community/netinfo axios
For iOS and Android, link SQLite properly:
cd ios && pod install
Setting up the Project
Create a new React Native project:
npx react-native init OfflineFirstApp
cd OfflineFirstApp
Using SQLite with react-native-sqlite-storage
✅ Initialize SQLite
// db.js
import SQLite from 'react-native-sqlite-storage';
SQLite.enablePromise(true);
export const getDBConnection = async () => {
return await SQLite.openDatabase({ name: 'offline.db', location: 'default' });
};
export const createTables = async (db) => {
const query = `CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
content TEXT,
synced INTEGER DEFAULT 0
)`;
await db.executeSql(query);
};
export const insertNote = async (db, note) => {
const insertQuery = 'INSERT INTO notes (title, content, synced) VALUES (?, ?, ?)';
await db.executeSql(insertQuery, [note.title, note.content, 0]);
};
export const getUnsyncedNotes = async (db) => {
const [results] = await db.executeSql('SELECT * FROM notes WHERE synced = 0');
return results;
};
const notes = [];
for (let i = 0; i < results.rows.length; i++) {
notes.push(results.rows.item(i));
}
return notes;
};
export const markAsSynced = async (db, noteId) => {
await db.executeSql(
`UPDATE notes SET synced = 1 WHERE id = ?`,
[noteId]
);
};
Detecting Network Status
// useNetwork.js
import { useEffect, useState } from 'react';
import NetInfo from '@react-native-community/netinfo';
const useNetwork = () => {
const [isConnected, setIsConnected] = useState(true);
useEffect(() => {
const unsubscribe = NetInfo.addEventListener(state => {
setIsConnected(state.isConnected);
});
return () => unsubscribe();
}, []);
return isConnected;
};
export default useNetwork;
Syncing Data with Remote API
Example sync function
// sync.js
import axios from 'axios';
import { getDBConnection, getUnsyncedNotes, markAsSynced } from './db';
export const syncNotes = async () => {
const db = await getDBConnection();
const unsyncedNotes = await getUnsyncedNotes(db);
for (const note of unsyncedNotes) {
try {
await axios.post('https://your-api.com/notes', {
title: note.title,
content: note.content,
});
await markAsSynced(db, note.id);
} catch (error) {
console.log('Sync failed for note:', note.id);
}
}
};
You can trigger syncNotes() when the network becomes available.
Handling Conflicts
Use timestamps and conflict resolution strategies:
- Last-write-wins
- Merge if both updated
- Prompt user for manual resolution
Update the DB schema:
ALTER TABLE notes ADD COLUMN updated_at TEXT;
Compare timestamps before syncing.
Example Use Case: Notes App
Create Note Screen
// CreateNote.js
import React, { useState } from 'react';
import { View, TextInput, Button } from 'react-native';
import { getDBConnection, insertNote } from './db';
import useNetwork from './useNetwork';
import { syncNotes } from './sync';
const CreateNote = () => {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const isConnected = useNetwork();
const saveNote = async () => {
const db = await getDBConnection();
await insertNote(db, { title, content });
if (isConnected) {
await syncNotes();
}
setTitle('');
setContent('');
};
return (
<View>
<TextInput placeholder="Title" value={title} onChangeText={setTitle} />
<TextInput placeholder="Content" value={content} onChangeText={setContent} />
<Button title="Save Note" onPress={saveNote} />
</View>
);
};
export default CreateNote;
Best Practices
- Use timestamps for syncing.
- Consider retry strategies with exponential backoff.
- Use queueing mechanisms to ensure order of operations.
- Optimize for battery by syncing on certain conditions (WiFi, charging).
- Encrypt sensitive local data if needed.
Optional Enhancements
- Use WatermelonDB or Realm for complex apps.
- Background sync using react-native-background-fetch.
- Use redux-persist to persist the UI state.
- Add UI indicators for “syncing” state.
Conclusion
Implementing an offline-first architecture in React Native greatly improves UX, especially in real-world conditions where connectivity is unreliable. With local databases, smart sync logic, and proper network detection, you can ensure a robust user experience.
End to End Technology Solutions