AsyncStorage - Persistance Locale
AsyncStorage est le système de stockage local pour React Native. Il joue le même rôle que localStorage sur le web : stocker des données clé-valeur qui persistent entre les sessions. La différence principale : toutes les opérations sont asynchrones (d'où le nom).
Comparaison avec localStorage
| Fonctionnalité | Web localStorage | React Native AsyncStorage |
|---|---|---|
| Écriture | localStorage.setItem(key, value) | await AsyncStorage.setItem(key, value) |
| Lecture | localStorage.getItem(key) | await AsyncStorage.getItem(key) |
| Suppression | localStorage.removeItem(key) | await AsyncStorage.removeItem(key) |
| Type de valeur | Chaînes uniquement | Chaînes uniquement |
| Synchrone / Asynchrone | Synchrone (bloquant) | Asynchrone (async/await) |
| Persistance | Jusqu'à suppression explicite | Jusqu'à désinstallation de l'app |
| Chiffrement | Non chiffré | Non chiffré |
Pourquoi asynchrone ?
Sur le web, localStorage est synchrone : l'opération bloque l'exécution jusqu'à la fin. Sur mobile, le stockage peut être plus lent (mémoire flash), et bloquer le thread principal provoquerait un gel de l'interface. AsyncStorage est asynchrone pour ne pas bloquer l'affichage pendant la lecture/écriture des données.
Installation
AsyncStorage n'est pas inclus dans React Native par défaut. Il faut l'installer :
npx expo install @react-native-async-storage/async-storage
npx expo install
Utilisez npx expo install au lieu de npm install pour garantir la compatibilité avec votre version du SDK Expo. C'est la même recommandation qu'en Séance 1 pour tous les packages.
API de base
Écrire une valeur
import AsyncStorage from '@react-native-async-storage/async-storage';
// Stocker une chaîne
await AsyncStorage.setItem('username', 'Alice');
// Stocker un objet (il faut le convertir en chaîne JSON)
const user = { name: 'Alice', age: 25 };
await AsyncStorage.setItem('user', JSON.stringify(user));
Lire une valeur
// Lire une chaîne
const username = await AsyncStorage.getItem('username');
console.log(username); // 'Alice'
// Lire un objet (il faut le parser depuis JSON)
const userData = await AsyncStorage.getItem('user');
const user = userData ? JSON.parse(userData) : null;
console.log(user); // { name: 'Alice', age: 25 }
Supprimer une valeur
// Supprimer une clé
await AsyncStorage.removeItem('username');
// Supprimer toutes les données (attention !)
await AsyncStorage.clear();
Pourquoi faut-il utiliser JSON.stringify() et JSON.parse() avec AsyncStorage ?
Pattern complet : charger et sauvegarder
Le pattern le plus courant avec AsyncStorage combine useEffect pour charger les données au démarrage et sauvegarder automatiquement quand elles changent :
import { useState, useEffect } from 'react';
import { View, Text, Pressable, StyleSheet } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
function Counter() {
const [count, setCount] = useState(0);
const [isLoaded, setIsLoaded] = useState(false);
// Charger la valeur sauvegardée au démarrage
useEffect(() => {
const loadCount = async () => {
try {
const saved = await AsyncStorage.getItem('counter');
if (saved !== null) {
setCount(parseInt(saved, 10));
}
} catch (error) {
console.error('Erreur de chargement :', error);
} finally {
setIsLoaded(true);
}
};
loadCount();
}, []);
// Sauvegarder à chaque changement
useEffect(() => {
if (isLoaded) { // Ne pas sauvegarder avant le chargement initial
AsyncStorage.setItem('counter', count.toString());
}
}, [count, isLoaded]);
if (!isLoaded) {
return <Text>Chargement...</Text>;
}
return (
<View style={styles.container}>
<Text style={styles.count}>{count}</Text>
<Pressable style={styles.button} onPress={() => setCount(count + 1)}>
<Text style={styles.buttonText}>+1</Text>
</Pressable>
<Text style={styles.info}>La valeur est sauvegardée automatiquement</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
count: { fontSize: 64, fontWeight: 'bold', color: '#0066cc' },
button: { backgroundColor: '#0066cc', borderRadius: 8, padding: 16, marginTop: 16 },
buttonText: { color: '#fff', fontSize: 20, fontWeight: 'bold' },
info: { marginTop: 16, fontSize: 14, color: '#666' },
});
Le flag isLoaded est important
Sans le flag isLoaded, le deuxième useEffect sauvegarderait la valeur initiale (0) avant que le chargement soit terminé, écrasant la valeur sauvegardée. Le flag garantit que la sauvegarde ne commence qu'après le chargement initial.
Que se passe-t-il si on ne vérifie pas isLoaded avant de sauvegarder ?
Gestion des erreurs
Les opérations AsyncStorage peuvent échouer (stockage plein, corruption des données). Gérez toujours les erreurs avec try/catch :
// ❌ Sans gestion d'erreur
const data = await AsyncStorage.getItem('key');
// ✅ Avec gestion d'erreur
const loadData = async () => {
try {
const data = await AsyncStorage.getItem('key');
if (data !== null) {
return JSON.parse(data);
}
return null;
} catch (error) {
console.error('Erreur AsyncStorage :', error);
return null;
}
};
const saveData = async (key, value) => {
try {
await AsyncStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Erreur de sauvegarde :', error);
}
};
JSON.parse() peut aussi échouer
Si les données sauvegardées sont corrompues ou dans un format inattendu, JSON.parse() lance une erreur. Mettez toujours JSON.parse() dans un bloc try/catch. C'est la même précaution que pour localStorage sur le web.
Exemple complet : préférences utilisateur
import { useState, useEffect } from 'react';
import { View, Text, Pressable, Switch, StyleSheet } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
const STORAGE_KEY = '@user_preferences';
const defaultPreferences = {
darkMode: false,
notifications: true,
fontSize: 'medium', // 'small', 'medium', 'large'
};
export default function SettingsScreen() {
const [prefs, setPrefs] = useState(defaultPreferences);
const [isLoaded, setIsLoaded] = useState(false);
// Charger les préférences au démarrage
useEffect(() => {
const loadPreferences = async () => {
try {
const saved = await AsyncStorage.getItem(STORAGE_KEY);
if (saved) {
setPrefs({ ...defaultPreferences, ...JSON.parse(saved) });
}
} catch (error) {
console.error('Erreur chargement préférences :', error);
} finally {
setIsLoaded(true);
}
};
loadPreferences();
}, []);
// Sauvegarder quand les préférences changent
useEffect(() => {
if (isLoaded) {
AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(prefs))
.catch(error => console.error('Erreur sauvegarde :', error));
}
}, [prefs, isLoaded]);
const updatePref = (key, value) => {
setPrefs(prev => ({ ...prev, [key]: value }));
};
if (!isLoaded) {
return (
<View style={styles.loading}>
<Text>Chargement des préférences...</Text>
</View>
);
}
return (
<View style={styles.container}>
<Text style={styles.title}>Paramètres</Text>
{/* Mode sombre */}
<View style={styles.settingRow}>
<View>
<Text style={styles.settingLabel}>Mode sombre</Text>
<Text style={styles.settingDesc}>Activer le thème sombre</Text>
</View>
<Switch
value={prefs.darkMode}
onValueChange={(value) => updatePref('darkMode', value)}
trackColor={{ false: '#ddd', true: '#81b0ff' }}
thumbColor={prefs.darkMode ? '#0066cc' : '#f4f3f4'}
/>
</View>
{/* Notifications */}
<View style={styles.settingRow}>
<View>
<Text style={styles.settingLabel}>Notifications</Text>
<Text style={styles.settingDesc}>Recevoir les alertes</Text>
</View>
<Switch
value={prefs.notifications}
onValueChange={(value) => updatePref('notifications', value)}
trackColor={{ false: '#ddd', true: '#81b0ff' }}
thumbColor={prefs.notifications ? '#0066cc' : '#f4f3f4'}
/>
</View>
{/* Taille du texte */}
<View style={styles.settingSection}>
<Text style={styles.settingLabel}>Taille du texte</Text>
<View style={styles.fontSizeRow}>
{['small', 'medium', 'large'].map(size => (
<Pressable
key={size}
style={[
styles.fontSizeButton,
prefs.fontSize === size && styles.fontSizeButtonActive,
]}
onPress={() => updatePref('fontSize', size)}
>
<Text style={[
styles.fontSizeText,
prefs.fontSize === size && styles.fontSizeTextActive,
]}>
{size === 'small' ? 'Petit' : size === 'medium' ? 'Moyen' : 'Grand'}
</Text>
</Pressable>
))}
</View>
</View>
{/* Réinitialiser */}
<Pressable
style={styles.resetButton}
onPress={() => setPrefs(defaultPreferences)}
>
<Text style={styles.resetText}>Réinitialiser les paramètres</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16, backgroundColor: '#f5f5f5' },
loading: { flex: 1, justifyContent: 'center', alignItems: 'center' },
title: { fontSize: 24, fontWeight: 'bold', marginBottom: 24 },
settingRow: {
flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center',
backgroundColor: '#fff', padding: 16, borderRadius: 12, marginBottom: 8,
},
settingSection: {
backgroundColor: '#fff', padding: 16, borderRadius: 12, marginBottom: 8,
},
settingLabel: { fontSize: 16, fontWeight: '600' },
settingDesc: { fontSize: 13, color: '#666', marginTop: 2 },
fontSizeRow: { flexDirection: 'row', gap: 8, marginTop: 12 },
fontSizeButton: {
flex: 1, padding: 10, borderRadius: 8, borderWidth: 1,
borderColor: '#ddd', alignItems: 'center',
},
fontSizeButtonActive: { borderColor: '#0066cc', backgroundColor: '#e8f0fe' },
fontSizeText: { fontSize: 14, color: '#666' },
fontSizeTextActive: { color: '#0066cc', fontWeight: '600' },
resetButton: {
padding: 16, borderRadius: 12, alignItems: 'center', marginTop: 16,
backgroundColor: '#fff', borderWidth: 1, borderColor: '#f44336',
},
resetText: { color: '#f44336', fontWeight: '600' },
});
Cet exemple montre :
- Chargement initial avec
useEffectet un état de chargement - Sauvegarde automatique avec
useEffectet dépendance surprefs - Valeurs par défaut fusionnées avec les données sauvegardées (
...defaultPreferences, ...saved) - Gestion d'erreurs avec try/catch
- Composant Switch pour les toggles (booléens)
Pourquoi fusionner les préférences par défaut avec les données sauvegardées ?
Comment stocker un tableau dans AsyncStorage ?
Bonnes pratiques
Conventions pour les clés
Préfixez vos clés AsyncStorage avec @ et le nom de votre app pour éviter les conflits :
const KEYS = {
preferences: '@myapp_preferences',
todos: '@myapp_todos',
authToken: '@myapp_auth_token',
};
Ce qu'il ne faut PAS stocker dans AsyncStorage
AsyncStorage n'est pas chiffré. Ne stockez jamais :
- Des mots de passe en clair
- Des tokens d'authentification sensibles (utilisez
expo-secure-storepour cela) - Des données confidentielles
AsyncStorage est adapté pour : préférences utilisateur, brouillons, données de cache, listes de tâches, paramètres d'affichage.
À retenir
Comprendre, pas mémoriser
AsyncStorage en résumé :
- Équivalent mobile de
localStorage: stockage clé-valeur persistant - Asynchrone : toujours utiliser
awaitou.then() - Chaînes uniquement :
JSON.stringify()pour écrire,JSON.parse()pour lire - Gestion d'erreurs : toujours entourer de try/catch
- Non chiffré : pas pour les données sensibles
Pattern standard :
// Charger au montage
useEffect(() => {
AsyncStorage.getItem('key').then(data => {
if (data) setState(JSON.parse(data));
setIsLoaded(true);
});
}, []);
// Sauvegarder quand ça change
useEffect(() => {
if (isLoaded) {
AsyncStorage.setItem('key', JSON.stringify(state));
}
}, [state, isLoaded]);