Introduction
Offline-first apps work without internet and sync when connected. This guide covers strategies and implementation for building offline-capable mobile apps.
Offline-First Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Offline-First Data Flow โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โ
โ โ UI Layer โโโโโโโโโโถโ Local Storage โ โ
โ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โ
โ โ โ โ
โ โ Online? โ โ
โ โผ โผ โ
โ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โ
โ โ API Client โโโโโโโโโโถโ Sync Engine โ โ
โ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโ โ
โ โ Remote API โ โ
โ โโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Local Storage Options
// 1. AsyncStorage - Simple key-value
import AsyncStorage from '@react-native-async-storage/async-storage';
// Save
await AsyncStorage.setItem('user', JSON.stringify(user));
await AsyncStorage.setItem('settings', JSON.stringify(settings));
// Load
const user = JSON.parse(await AsyncStorage.getItem('user'));
// 2. SQLite - Full database
// npm install expo-sqlite
import * as SQLite from 'expo-sqlite';
const db = await SQLite.openDatabaseAsync('mydb');
// Create table
await db.execAsync(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
name TEXT,
email TEXT,
synced INTEGER DEFAULT 0
);
`);
// 3. WatermelonDB - Reactive database
// Best for complex offline apps
Sync Strategies
Optimistic Updates
// Save locally first, then sync
async function createPost(content: string) {
const post = {
id: generateId(),
content,
createdAt: new Date().toISOString(),
synced: false,
};
// 1. Save to local DB immediately
await db.posts.create(post);
// 2. Update UI (optimistic)
addPostToList(post);
// 3. Try to sync in background
try {
await api.createPost(post);
await db.posts.update(post.id, { synced: true });
} catch (error) {
// Will retry later
console.log('Sync failed, will retry');
}
}
Background Sync
// Sync service
import * as BackgroundFetch from 'expo-background-fetch';
async function syncPendingPosts() {
const pendingPosts = await db.posts
.where('synced', false)
.fetch();
for (const post of pendingPosts) {
try {
await api.createPost(post);
await db.posts.update(post.id, { synced: true });
} catch (error) {
// Continue with next
}
}
}
// Register background task
BackgroundFetch.registerTaskAsync('sync', {
minimumInterval: 15 * 60, // 15 minutes
}, syncPendingPosts);
Conflict Resolution
Strategies
conflict_resolution:
last_write_wins:
- "Simple: most recent wins"
- "Good for non-critical data"
server_wins:
- "Server always correct"
- "Client changes overwritten"
client_wins:
- "Client always correct"
- "For user-generated content"
merge:
- "Combine changes intelligently"
- "Complex but powerful"
Implementation
async function syncWithConflictResolution(localPost, serverPost) {
// Compare timestamps
const localTime = new Date(localPost.updatedAt);
const serverTime = new Date(serverPost.updatedAt);
if (localTime > serverTime) {
// Local is newer - push to server
await api.updatePost(localPost.id, localPost);
} else {
// Server is newer - update local
await db.posts.update(localPost.id, serverPost);
}
}
Key Takeaways
- Local-first - Save locally, sync when online
- AsyncStorage - Simple key-value storage
- SQLite - Full database capabilities
- Optimistic UI - Update immediately, sync later
Comments