ScaleRocket/Mobile

Support hors ligne

Patterns offline-first avec cache AsyncStorage, détection de connectivité NetInfo et file d'attente de mutations.

Support hors ligne

Les applications mobiles doivent gérer les interruptions réseau avec élégance. ScaleRocket Mobile fournit des patterns pour mettre en cache les données, détecter la connectivité et mettre en file d'attente les mutations quand l'utilisateur revient en ligne.

Détecter la connectivité

Utilisez @react-native-community/netinfo pour vérifier l'état du réseau :

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;
}

Affichez une bannière hors ligne quand déconnecté :

function OfflineBanner() {
  const isConnected = useNetworkStatus();

  if (isConnected) return null;

  return (
    <View style={styles.banner}>
      <Text style={styles.bannerText}>You are offline</Text>
    </View>
  );
}

Mettre en cache avec AsyncStorage

Mettez en cache les réponses API pour que l'app fonctionne sans réseau :

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) {
    // Réseau échoué — essayer le cache
    const cached = await AsyncStorage.getItem(key);
    if (cached) return JSON.parse(cached);
    throw error;
  }
}

Utilisation :

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 };
}

File d'attente de mutations

Mettez en file d'attente les écritures hors ligne et rejouez-les quand la connectivité revient :

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));
}

Traitement automatique quand la connectivité revient :

useEffect(() => {
  const unsubscribe = NetInfo.addEventListener((state) => {
    if (state.isConnected) {
      processQueue();
    }
  });
  return () => unsubscribe();
}, []);

Patterns spécifiques au backend

Supabase ne fournit pas de support hors ligne intégré. Utilisez les patterns de cache et de file d'attente ci-dessus. Pour les mises à jour optimistes :

async function updateProfileOptimistic(name: string) {
  // Mettre à jour l'UI immédiatement
  setProfile((prev) => prev ? { ...prev, name } : prev);

  try {
    await supabase.from("profiles").update({ name }).eq("id", userId);
  } catch {
    // Annuler en cas d'échec
    setProfile((prev) => prev ? { ...prev, name: originalName } : prev);
    await queueMutation("updateProfile", { name });
  }
}

Convex gère les mises à jour optimistes automatiquement pour les mutations. Les requêtes montrent des données périmées si hors ligne mais reprennent la sync temps réel à la reconnexion :

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,
      });
    }
  });

Bonnes pratiques

PratiquePourquoi
Mettre en cache les données lues agressivementLes utilisateurs s'attendent à voir du contenu même hors ligne
Mettre en file d'attente les écritures, ne pas les perdrePerdre les saisies utilisateur est inacceptable
Montrer les données périmées avec indicateur"Mis à jour il y a 5 min" est mieux qu'un écran vide
Traiter la file d'attente dans l'ordreMaintenir la cohérence des données
Définir une expiration sur les données en cacheNe pas montrer des données vieilles de plusieurs semaines

On this page