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/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;
}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
| Pratique | Pourquoi |
|---|---|
| Mettre en cache les données lues agressivement | Les utilisateurs s'attendent à voir du contenu même hors ligne |
| Mettre en file d'attente les écritures, ne pas les perdre | Perdre 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'ordre | Maintenir la cohérence des données |
| Définir une expiration sur les données en cache | Ne pas montrer des données vieilles de plusieurs semaines |