Cours React Native · Module 4: État et données d'API

Zustand - Un store léger

3 min de lecture ·
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 zustand

Store 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. 1. Comment crée-t-on un store avec Zustand ?

  2. 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.

Articles utiles