Offline Support
Offline-first patterns with AsyncStorage caching, NetInfo for connectivity detection, and mutation queuing.
Offline Support
Mobile apps need to handle network interruptions gracefully. ScaleRocket Mobile provides patterns for caching data, detecting connectivity, and queuing mutations for when the user comes back online.
Detect Connectivity
Use @react-native-community/netinfo to check network status:
npx expo install @react-native-community/netinfoimport NetInfo from "@react-native-community/netinfo";
function useNetworkStatus() {
const [isConnected, setIsConnected] = useState(true);
useEffect(() => {
const unsubscribe = NetInfo.addEventListener((state) => {
setIsConnected(state.isConnected ?? true);
});
return () => unsubscribe();
}, []);
return isConnected;
}Show an offline banner when disconnected:
function OfflineBanner() {
const isConnected = useNetworkStatus();
if (isConnected) return null;
return (
<View style={styles.banner}>
<Text style={styles.bannerText}>You are offline</Text>
</View>
);
}Cache Data with AsyncStorage
Cache API responses so the app works without network:
import AsyncStorage from "@react-native-async-storage/async-storage";
async function fetchWithCache<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
try {
const fresh = await fetcher();
await AsyncStorage.setItem(key, JSON.stringify(fresh));
return fresh;
} catch (error) {
// Network failed — try cache
const cached = await AsyncStorage.getItem(key);
if (cached) return JSON.parse(cached);
throw error;
}
}Usage:
function useCachedProfile(userId: string) {
const [profile, setProfile] = useState<Profile | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetchWithCache(`profile-${userId}`, () => fetchProfile(userId))
.then(setProfile)
.finally(() => setIsLoading(false));
}, [userId]);
return { profile, isLoading };
}Mutation Queue
Queue writes when offline and replay them when connectivity returns:
import AsyncStorage from "@react-native-async-storage/async-storage";
import NetInfo from "@react-native-community/netinfo";
interface QueuedMutation {
id: string;
action: string;
payload: any;
timestamp: number;
}
async function queueMutation(action: string, payload: any) {
const queue = await getQueue();
queue.push({
id: Date.now().toString(),
action,
payload,
timestamp: Date.now(),
});
await AsyncStorage.setItem("mutation-queue", JSON.stringify(queue));
}
async function getQueue(): Promise<QueuedMutation[]> {
const raw = await AsyncStorage.getItem("mutation-queue");
return raw ? JSON.parse(raw) : [];
}
async function processQueue() {
const queue = await getQueue();
const failed: QueuedMutation[] = [];
for (const mutation of queue) {
try {
await executeMutation(mutation);
} catch {
failed.push(mutation);
}
}
await AsyncStorage.setItem("mutation-queue", JSON.stringify(failed));
}Auto-process when connectivity returns:
useEffect(() => {
const unsubscribe = NetInfo.addEventListener((state) => {
if (state.isConnected) {
processQueue();
}
});
return () => unsubscribe();
}, []);Backend-Specific Patterns
Supabase does not provide built-in offline support. Use the caching and queue patterns above. For optimistic updates:
async function updateProfileOptimistic(name: string) {
// Update UI immediately
setProfile((prev) => prev ? { ...prev, name } : prev);
try {
await supabase.from("profiles").update({ name }).eq("id", userId);
} catch {
// Revert on failure
setProfile((prev) => prev ? { ...prev, name: originalName } : prev);
await queueMutation("updateProfile", { name });
}
}Convex handles optimistic updates automatically for mutations. Queries will show stale data if offline but resume real-time sync when reconnected:
const updateProfile = useMutation(api.users.updateProfile)
.withOptimisticUpdate((localStore, args) => {
const current = localStore.getQuery(api.users.getProfile);
if (current) {
localStore.setQuery(api.users.getProfile, {}, {
...current,
name: args.name,
});
}
});Best Practices
| Practice | Why |
|---|---|
| Cache read data aggressively | Users expect to see content even offline |
| Queue writes, don't drop them | Losing user input is unacceptable |
| Show stale data with indicator | "Updated 5 min ago" is better than a blank screen |
| Process queue in order | Maintain data consistency |
| Set expiry on cached data | Don't show data that's weeks old |