ScaleRocket/Mobile

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/netinfo
import 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

PracticeWhy
Cache read data aggressivelyUsers expect to see content even offline
Queue writes, don't drop themLosing user input is unacceptable
Show stale data with indicator"Updated 5 min ago" is better than a blank screen
Process queue in orderMaintain data consistency
Set expiry on cached dataDon't show data that's weeks old

On this page