Cours React Native · Module 4: État et données d'API
Zustand - Un store léger
Dans cette leçon6
Zustand est une bibliothèque minimaliste de gestion d'état - aussi simple que useState, mais globale comme Redux !
Installation
npx expo install zustandStore basique
// stores/treasureStore.ts
import { create } from 'zustand';
interface Treasure {
id: string;
name: string;
value: number;
}
interface TreasureStore {
// State
treasures: Treasure[];
isLoading: boolean;
error: string | null;
// Actions
addTreasure: (treasure: Omit<Treasure, 'id'>) => void;
removeTreasure: (id: string) => void;
setLoading: (loading: boolean) => void;
setError: (error: string | null) => void;
reset: () => void;
}
export const useTreasureStore = create<TreasureStore>((set) => ({
// État initial
treasures: [],
isLoading: false,
error: null,
// Actions
addTreasure: (treasure) =>
set((state) => ({
treasures: [...state.treasures, { ...treasure, id: Date.now().toString() }],
})),
removeTreasure: (id) =>
set((state) => ({
treasures: state.treasures.filter((t) => t.id !== id),
})),
setLoading: (isLoading) => set({ isLoading }),
setError: (error) => set({ error, isLoading: false }),
reset: () => set({ treasures: [], isLoading: false, error: null }),
}));Utilisation dans les composants
import { useTreasureStore } from '@/stores/treasureStore';
function TreasureList() {
// S'abonner à l'état entier
const { treasures, isLoading, removeTreasure } = useTreasureStore();
// Ou abonnement sélectif (meilleures performances)
const treasures = useTreasureStore((state) => state.treasures);
const removeTreasure = useTreasureStore((state) => state.removeTreasure);
if (isLoading) return <ActivityIndicator />;
return (
<FlatList
data={treasures}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<Pressable onPress={() => removeTreasure(item.id)}>
<Text>{item.name}</Text>
</Pressable>
)}
/>
);
}Actions asynchrones
import { create } from 'zustand';
interface TreasureStore {
treasures: Treasure[];
isLoading: boolean;
error: string | null;
fetchTreasures: () => Promise<void>;
sellTreasure: (id: string) => Promise<void>;
}
export const useTreasureStore = create<TreasureStore>((set, get) => ({
treasures: [],
isLoading: false,
error: null,
fetchTreasures: async () => {
set({ isLoading: true, error: null });
try {
const response = await fetch('https://api.atlantis.app/treasures');
const data = await response.json();
set({ treasures: data, isLoading: false });
} catch (error) {
set({ error: 'Échec de la récupération des trésors', isLoading: false });
}
},
sellTreasure: async (id: string) => {
const treasure = get().treasures.find((t) => t.id === id);
if (!treasure) return;
try {
await fetch(`https://api.atlantis.app/treasures/${id}/sell`, {
method: 'POST',
});
// Retirer après une vente réussie
set((state) => ({
treasures: state.treasures.filter((t) => t.id !== id),
}));
} catch (error) {
set({ error: 'Échec de la vente du trésor' });
}
},
}));Middleware - Persist
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
export const useTreasureStore = create<TreasureStore>()(
persist(
(set) => ({
treasures: [],
isLoading: false,
error: null,
addTreasure: (treasure) =>
set((state) => ({
treasures: [...state.treasures, { ...treasure, id: Date.now().toString() }],
})),
// ... reste des actions
}),
{
name: 'treasure-storage', // clé dans le stockage
storage: createJSONStorage(() => AsyncStorage),
partialize: (state) => ({ treasures: state.treasures }), // ce qu'il faut sauvegarder
}
)
);Sélecteurs et performance
// Mauvais - le composant se re-rend à chaque changement d'état
function BadComponent() {
const store = useTreasureStore();
return <Text>{store.treasures.length}</Text>;
}
// Bon - le composant ne se re-rend que lorsque treasures.length change
function GoodComponent() {
const count = useTreasureStore((state) => state.treasures.length);
return <Text>{count}</Text>;
}
// Sélecteurs avancés
import { shallow } from 'zustand/shallow';
function TreasureSummary() {
const { total, legendary } = useTreasureStore(
(state) => ({
total: state.treasures.length,
legendary: state.treasures.filter((t) => t.rarity === 'legendary').length,
}),
shallow // comparaison superficielle plutôt que par référence
);
return (
<View>
<Text>Total : {total}</Text>
<Text>Légendaires : {legendary}</Text>
</View>
);
}Zustand est un excellent choix pour la plupart des applications - simple, performant et sans boilerplate !
Code de cette leçon : store.ts
import { create } from 'zustand';
export interface Treasure {
id: string;
name: string;
value: number;
}
interface TreasureStore {
treasures: Treasure[];
addTreasure: (name: string, value: number) => void;
removeTreasure: (id: string) => void;
reset: () => void;
}
export const useTreasureStore = create<TreasureStore>((set) => ({
// État initial
treasures: [],
// TODO : ajoute un trésor au tableau avec un id unique (Date.now().toString())
addTreasure: (name, value) =>
set((state) => ({
treasures: [...state.treasures, { id: Date.now().toString(), name, value }],
})),
// TODO : supprime le trésor correspondant à l'id donné à l'aide de filter
removeTreasure: (id) =>
set((state) => ({
treasures: state.treasures.filter((t) => t.id !== id),
})),
// TODO : vide entièrement le tableau de trésors
reset: () => set({ treasures: [] }),
}));Entrées de la documentation utilisées dans cette leçon
Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Comment crée-t-on un store avec Zustand ?
2. Pourquoi vaut-il mieux utiliser des sélecteurs avec Zustand ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Clique dans l'ordre
Clique sur les éléments pour mettre à jour l'état avec Zustand :
- Éditeur de code
Complète le code du store avec les actions CRUD.