Handling Offline Support in Expo Apps
Introduction
Offline support ensures your app remains functional without network connectivity. This guide covers caching, local storage, and data synchronization in Expo.
Prerequisites
- Expo project
- Basic React Native knowledge
Step 1: Install Dependencies
expo install expo-sqlite @react-native-async-storage/async-storage @nozbe/watermelondb
Step 2: Choose Storage Strategy
- AsyncStorage: Simple key-value storage
- SQLite: Relational storage for complex data
- WatermelonDB: High-performance database with sync
Step 3: Setup AsyncStorage for Caching
Create utils/cache.js
:
import AsyncStorage from "@react-native-async-storage/async-storage";
export async function setCache(key, value) {
try {
await AsyncStorage.setItem(key, JSON.stringify(value));
} catch (e) {
console.error("Failed to save cache:", e);
}
}
export async function getCache(key) {
try {
const json = await AsyncStorage.getItem(key);
return json != null ? JSON.parse(json) : null;
} catch (e) {
console.error("Failed to load cache:", e);
return null;
}
}
Step 4: Use SQLite for Structured Data
Create utils/db.js
:
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("app.db");
export function initDatabase() {
db.transaction(tx => {
tx.executeSql(
`CREATE TABLE IF NOT EXISTS items (
id TEXT PRIMARY KEY NOT NULL,
data TEXT NOT NULL
);`
);
});
}
export function insertItem(id, data) {
db.transaction(tx => {
tx.executeSql("INSERT OR REPLACE INTO items (id, data) VALUES (?, ?);", [
id,
JSON.stringify(data),
]);
});
}
export function getItems(callback) {
db.transaction(tx => {
tx.executeSql("SELECT * FROM items;", [], (_, { rows }) =>
callback(rows._array)
);
});
}
Step 5: Setup WatermelonDB for Sync
Initialize in db/schema.js
:
import { appSchema, tableSchema } from "@nozbe/watermelondb";
export const mySchema = appSchema({
version: 1,
tables: [
tableSchema({
name: "tasks",
columns: [
{ name: "title", type: "string" },
{ name: "completed", type: "boolean" },
],
}),
],
});
In db/index.js
:
import { Database } from "@nozbe/watermelondb";
import SQLiteAdapter from "@nozbe/watermelondb/adapters/sqlite";
import { mySchema } from "./schema";
import Task from "./models/Task";
const adapter = new SQLiteAdapter({ schema: mySchema });
export const database = new Database({
adapter,
modelClasses: [Task],
});
Step 6: Implement Sync Logic
In sync.js
:
import { database } from "./db";
import { synchronize } from "@nozbe/watermelondb/sync";
export async function sync() {
try {
await synchronize({
database,
pullChanges: async ({ lastPulledAt }) => {
const res = await fetch(
`https://api.example.com/sync?lastPulledAt=${lastPulledAt}`
);
return res.json();
},
pushChanges: async ({ changes }) => {
await fetch("https://api.example.com/sync", {
method: "POST",
body: JSON.stringify(changes),
});
},
});
} catch (error) {
console.error("Sync failed:", error);
}
}
Step 7: Offline-First Pattern Example
In component:
import React, { useEffect, useState } from "react";
import { View, Text, FlatList } from "react-native";
import { getCache, setCache } from "@/utils/cache";
import { getItems } from "@/utils/db";
export default function OfflineFirstList() {
const [items, setItemsState] = useState([]);
useEffect(() => {
// Load from local storage
getCache("items").then(cached => {
if (cached) setItemsState(cached);
});
// Fetch from network
fetch("https://api.example.com/items")
.then(res => res.json())
.then(async data => {
setItemsState(data);
await setCache("items", data);
})
.catch(() => console.log("Network fetch failed, using cache"));
}, []);
return (
<FlatList
data={items}
keyExtractor={item => item.id}
renderItem={({ item }) => <Text>{item.name}</Text>}
/>
);
}
Summary
Offline support in Expo can be achieved using AsyncStorage, SQLite, or WatermelonDB with sync. Implement caching and sync strategies for seamless user experiences.