Replay

Apprendre Angular en 1h

Je me donne un objectif: vous faire découvrir et apprendre Angular en 1 heure: composant, syntaxe dans les templates, les directives, les signaux, les routeurs, les services, l'injection de dépendances, les observables et les requêtes HTTP. Le nécessaire pour faire une application Angular !.

Skip to content

Vous souhaitez recevoir de l'aide sur ce sujet ? rejoignez la communauté Angular.fr sur Discord.

Écriture de l'état dans NgRx Signal Store

L'écriture de l'état est une opération fondamentale dans toute application utilisant une gestion d'état. NgRx Signal Store propose une approche simple et intuitive pour modifier l'état, tout en garantissant l'immutabilité et la prévisibilité des changements.

L'écriture d'état dans la vie quotidienne

Pour comprendre l'écriture d'état, pensez à la mise à jour d'un agenda. Lorsque vous planifiez un nouveau rendez-vous, vous n'arrachez pas la page de votre agenda pour en créer une nouvelle - vous ajoutez simplement une entrée à l'emplacement approprié. De même, lorsque vous modifiez l'état dans un Signal Store, vous ne recréez pas l'intégralité de l'état mais vous spécifiez précisément quelles parties doivent être mises à jour.

withMethods et patchState

Dans NgRx Signal Store, les méthodes de modification de l'état sont définies à l'aide de withMethods. À l'intérieur de ces méthodes, on utilise généralement patchState pour effectuer les modifications :

ts
import { signalStore, withState, withMethods, patchState } from '@ngrx/signals';

interface CounterState {
  count: number;
  lastUpdated: Date | null;
}

export const CounterStore = signalStore(
  withState<CounterState>({
    count: 0,
    lastUpdated: null
  }),
  withMethods((store) => ({
    // Méthode pour incrémenter le compteur
    increment() {
      patchState(store, (state) => ({
        count: state.count + 1,
        lastUpdated: new Date()
      }));
    },
    
    // Méthode pour décrémenter le compteur
    decrement() {
      patchState(store, (state) => ({
        count: state.count - 1,
        lastUpdated: new Date()
      }));
    },
    
    // Méthode pour définir une valeur spécifique
    setValue(newValue: number) {
      patchState(store, {
        count: newValue,
        lastUpdated: new Date()
      });
    },
    
    // Méthode pour réinitialiser le compteur
    reset() {
      patchState(store, {
        count: 0,
        lastUpdated: new Date()
      });
    }
  }))
);

IMMUTABILITÉ

patchState préserve l'immutabilité de l'état. Il crée une nouvelle version de l'état en fusionnant les changements avec l'état actuel, plutôt que de modifier l'état existant.

Deux façons d'utiliser patchState

Il existe deux façons principales d'utiliser patchState :

  1. Avec un objet partiel : Pour définir directement les nouvelles valeurs
ts
// Mise à jour directe
patchState(store, {
  count: 10,
  lastUpdated: new Date()
});
  1. Avec une fonction : Pour utiliser l'état actuel dans le calcul des nouvelles valeurs
ts
// Mise à jour basée sur l'état actuel
patchState(store, (state) => ({
  count: state.count + 1,
  lastUpdated: new Date()
}));

Mise à jour d'objets imbriqués

Pour mettre à jour des propriétés imbriquées dans un objet, vous devez respecter l'immutabilité :

ts
interface UserState {
  user: {
    profile: {
      name: string;
      email: string;
      preferences: {
        theme: 'light' | 'dark';
        notifications: boolean;
      }
    }
  }
}

// Dans une méthode du store
updateTheme(theme: 'light' | 'dark') {
  patchState(store, (state) => ({
    user: {
      ...state.user,
      profile: {
        ...state.user.profile,
        preferences: {
          ...state.user.profile.preferences,
          theme
        }
      }
    }
  }));
}

OBJETS IMBRIQUÉS

Les mises à jour d'objets imbriqués peuvent devenir vertigineuses. Dans ce cas, envisagez de restructurer votre état ou d'utiliser des bibliothèques comme Immer qui simplifient ces opérations.

Mise à jour des tableaux

Lors de la mise à jour de tableaux, vous devez également préserver l'immutabilité :

ts
interface TaskState {
  tasks: { id: number; text: string; completed: boolean }[];
}

// Ajouter une tâche
addTask(text: string) {
  const newTask = { id: Date.now(), text, completed: false };
  patchState(store, (state) => ({
    tasks: [...state.tasks, newTask]
  }));
}

// Supprimer une tâche
removeTask(id: number) {
  patchState(store, (state) => ({
    tasks: state.tasks.filter(task => task.id !== id)
  }));
}

// Mettre à jour une tâche
updateTask(id: number, updates: Partial<Task>) {
  patchState(store, (state) => ({
    tasks: state.tasks.map(task => 
      task.id === id 
        ? { ...task, ...updates } 
        : task
    )
  }));
}

// Basculer l'état completed d'une tâche
toggleTask(id: number) {
  patchState(store, (state) => ({
    tasks: state.tasks.map(task => 
      task.id === id 
        ? { ...task, completed: !task.completed } 
        : task
    )
  }));
}

Gestion des effets secondaires

Les méthodes de store peuvent également gérer des effets secondaires, comme sauvegarder l'état dans le localStorage :

ts
import { signalStore, withState, withMethods, patchState } from '@ngrx/signals';

export const PreferencesStore = signalStore(
  withState({
    theme: 'light',
    fontSize: 'medium',
    language: 'fr'
  }),
  withMethods((store) => ({
    // Méthode pour mettre à jour le thème et le sauvegarder
    setTheme(theme: 'light' | 'dark') {
      // Mettre à jour l'état
      patchState(store, { theme });
      
      // Effet secondaire : sauvegarder dans localStorage
      localStorage.setItem('theme', theme);
    },
    
    // Méthode pour mettre à jour toutes les préférences
    updatePreferences(prefs: Partial<typeof store>) {
      // Mettre à jour l'état
      patchState(store, prefs);
      
      // Effet secondaire : sauvegarder dans localStorage
      localStorage.setItem('preferences', JSON.stringify({
        theme: store.theme(),
        fontSize: store.fontSize(),
        language: store.language(),
        ...prefs
      }));
    }
  }))
);

SÉPARATION DES PRÉOCCUPATIONS

Pour des applications plus complexes, il est préférable de séparer les effets secondaires des méthodes de mise à jour de l'état en utilisant withEffects ou rxMethod, ce qui sera abordé dans un autre tutoriel.

Méthodes prenant des paramètres

Les méthodes peuvent prendre des paramètres pour rendre les modifications d'état plus flexibles :

ts
interface CartState {
  items: { id: number; name: string; price: number; quantity: number }[];
  total: number;
}

export const CartStore = signalStore(
  withState<CartState>({
    items: [],
    total: 0
  }),
  withMethods((store) => ({
    /**
     * Ajoute un produit au panier ou augmente sa quantité s'il existe déjà
     * @param product Le produit à ajouter au panier
     */
    addToCart(product: { id: number; name: string; price: number }) {
      patchState(store, (state) => {
        const existingItem = state.items.find(item => item.id === product.id);
        
        if (existingItem) {
          // Produit déjà dans le panier, augmenter la quantité
          const updatedItems = state.items.map(item => 
            item.id === product.id 
              ? { ...item, quantity: item.quantity + 1 } 
              : item
          );
          
          return {
            items: updatedItems,
            total: calculateTotal(updatedItems)
          };
        } else {
          // Nouveau produit, l'ajouter au panier
          const newItem = { ...product, quantity: 1 };
          const updatedItems = [...state.items, newItem];
          
          return {
            items: updatedItems,
            total: calculateTotal(updatedItems)
          };
        }
      });
    },
    
    /**
     * Met à jour la quantité d'un produit dans le panier
     * @param productId ID du produit à mettre à jour
     * @param quantity Nouvelle quantité
     */
    updateQuantity(productId: number, quantity: number) {
      if (quantity <= 0) {
        this.removeFromCart(productId);
        return;
      }
      
      patchState(store, (state) => {
        const updatedItems = state.items.map(item => 
          item.id === productId 
            ? { ...item, quantity } 
            : item
        );
        
        return {
          items: updatedItems,
          total: calculateTotal(updatedItems)
        };
      });
    },
    
    /**
     * Supprime un produit du panier
     * @param productId ID du produit à supprimer
     */
    removeFromCart(productId: number) {
      patchState(store, (state) => {
        const updatedItems = state.items.filter(item => item.id !== productId);
        
        return {
          items: updatedItems,
          total: calculateTotal(updatedItems)
        };
      });
    },
    
    /**
     * Vide entièrement le panier
     */
    clearCart() {
      patchState(store, {
        items: [],
        total: 0
      });
    }
  }))
);

// Fonction utilitaire pour calculer le total
function calculateTotal(items: { price: number; quantity: number }[]) {
  return items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
}

Bonnes pratiques pour l'écriture d'état

  1. Granularité appropriée : Créez des méthodes qui représentent des actions significatives du domaine.

  2. Vérifications de validité : Incluez des validations pour éviter les états invalides.

ts
updateQuantity(productId: number, quantity: number) {
  // Valider les entrées
  if (quantity <= 0) {
    throw new Error('La quantité doit être supérieure à 0');
  }
  
  if (!Number.isInteger(quantity)) {
    throw new Error('La quantité doit être un nombre entier');
  }
  
  // Vérifier que le produit existe
  const productExists = store.items().some(item => item.id === productId);
  if (!productExists) {
    throw new Error(`Produit avec ID ${productId} non trouvé dans le panier`);
  }
  
  // Si tout est valide, mettre à jour l'état
  patchState(store, (state) => ({ /*...*/ }));
}
  1. Transactions atomiques : Si une opération nécessite plusieurs mises à jour, effectuez-les dans une seule opération patchState pour éviter des états intermédiaires indésirables.

Exemple complet

Voici un exemple complet d'écriture d'état pour un gestionnaire de tâches :