• 8 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 28/08/2024

Mettez à jour le store de façon asynchrone

Revenez sur l’asynchrone

JavaScript est un langage dit “single-thread”, c'est-à-dire que les tâches sont exécutées les unes après les autres sans possibilité d’exécuter deux choses ou plus à la fois.

Si vous arrivez à exécuter le script suivant dans votre navigateur, vous verrez que le temps qu’il se termine, rien d’autre ne pourra être fait :

let a = 0
const b = 10_000_000_000 // notation BIGINT
console.log(new Date())
for (let i = 0; i < b; i++) {
    a += i
}
console.log(a)
console.log(new Date())

Tous les autres scripts ou actions devront attendre la fin de celui-ci avant de s’exécuter.

Dans bien des cas, ce fonctionnement serait problématique. Par exemple, faire une requête de données pourrait bloquer la page.

Comment éviter ce comportement ?

Eh bien avec JavaScript, nous pouvons contourner ce problème en utilisant la programmation asynchrone. Vous connaissez sans doute le gestionnaire d'événements, la fameuse méthode addEventListener : c’est une approche de l’asynchrone.

Quel impact cette approche a-t-elle sur notre store Redux ? Et comment l’ajouter à notre store ?

Nous allons pour cela introduire un outil Redux, les middlewares, et ainsi utiliser createAsyncThunk  pour gérer l’asynchrone dans notre store.

Interceptez les changements du state avec les middlewares

Les middlewares sont en quelque sorte une pile de fonctions qui exécutent un traitement sur une donnée, puis la transmettent jusqu’à la fonction suivante jusqu'à ce qu'une des fonctions termine le programme ou qu’il n’y ait plus de fonction à la suite.

Je vous fais un petit schéma ci-dessous pour représenter le processus :

Une valeur en entrée est modifiée par deux fonctions avant que le programme se termine et elle est retournée.
Comment les middlewares fonctionnent

Les middlewares interceptent donc les valeurs et les modifient si nécessaire. Si on veut ajouter ou supprimer une valeur avant de laisser la main à la fonction suivante, par exemple, ou bien si on veut vérifier des données avant que l’application fasse un traitement sur ces données.

Les middlewares Redux fonctionnent de cette manière en interceptant les actions avant de les transmettre aux reducers. Il est donc possible de traiter les valeurs des payloads ou de modifier les types des actions avant que les reducers ne traitent les actions.

Cette fonctionnalité nous permet par exemple d’ajouter du logging, du reporting en cas de crash, de faire des appels à des API, et bien plus.

Je ne vais pas vous proposer de créer un middleware, mais pour mieux comprendre ses interactions avec les actions et le store, je vous suggère de me suivre dans ce screencast :

Si vous voulez essayez vous-même, voici le code que vous pouvez copier et coller dans le store :

export const store = configureStore(
  {
    preloadedState: state,
    reducer: combineReducers({
      owner: ownerSlice.reducer,
      list: cartSlice.reducer,
      notes: notesSlice.reducer,
    }),
    // Liste des middlewares
   middleware: (getDefaultMiddleware) =>
  getDefaultMiddleware().prepend([
    (store) => (next) => (action) => {
      console.log('Action', action);
      next(action);
    }
  ])

Ce middleware a un fonctionnement très simple : il reçoit l’action lorsque que l’on utilise le dispatch  du store, il affiche l’action dans la console, et transmet l’action au middleware suivant.

Vous devriez voir dans la console de votre navigateur s’afficher l’action exécutée, et cela, pour toutes les actions.

Utilisez les thunks pour modifier le store de façon asynchrone

Nous savons ce que sont les middlewares et nous maîtrisons le concept d’asynchrone. Voyons maintenant comment combiner ces deux aspects afin d’exécuter des traitements asynchrones dans notre store. Cela nous permettra d’utiliser les appels de données via le réseau, avec fetch  par exemple.

Pour cela, nous allons utiliser les thunks de Redux fournis par le middleware redux-thunk.

Unthunkest une fonction qui s'exécute de façon synchrone ou asynchrone et qui prend en paramètres les méthodes dispatch  et getState de notre store :

const thunkFunction = (dispatch, getState) => {
    // logic here that can dispatch actions or read state
}
store.dispatch(thunkFunction)

Comme pour les actions, il est préférable de créer des thunks actions creators, thunkCreator  , comme ci-dessous :

const thunkCreator = (arg1, arg2) => {
    return async (dispatch, getState) => {
      // on met ici la logique du thunk
    }
}

Nous allons maintenant créer notre premier thunk  .

Admettons que pour toute commande de Poulet Croquant, nous voulions demander au client s’il veut bénéficier d’une offre spéciale : 3 burgers ‘Poulet Croquant’ achetés, le troisième à moitié prix. Et pour cela, nous lui demandons dans une boîte de dialogue s'il veut en profiter en ajoutant un troisième poulet croquant 5 secondes après qu’il en a ajouté 2.

Suivez le screencast suivant pour comprendre comment j’ai implémenté cette fonctionnalité :

Nous avons donc modifié notre fichier features/menu/Menu.js  pour changer notre dispatch  d’ajout de produit.

Nous avons créé un thunk action creator et ajouté une confirmation de l’utilisateur. Celle-ci s’affiche au bout de 5 secondes après l’ajout du burger si celui-ci est de type Poulet Croquant et au nombre de 2 dans le panier :

import { useDispatch } from 'react-redux';
import * as ProductList from '../../common/models';
import { ProductCard } from '../../common/components/ProductCard';
import { cartSlice } from '../cart/cartSlice';
import { getListQuantityProductPerName } from '../../app/selectors';

const addProductThunk = (product) => (dispatch, getState) => {
    dispatch(cartSlice.actions.addProduct(product));
    return new Promise((resolve) => {
      setTimeout(() => {
        const state = getState();
        const numberProductPerName = getListQuantityProductPerName(state);
          const numberForSpecialOffer = numberProductPerName.find((item) => item.title === "Poulet Croquant")?.quantity;
        if (numberForSpecialOffer === 2) {
window.confirm("Voulez-vous ajouter une troisième fois ce produit à moitié prix ?")

            const specialOffer = ProductList.PouletCroquant
              dispatch(cartSlice.actions.addProduct({...specialOffer, price:       Math.round((ProductList.PouletCroquant.price / 2) * 100) / 100}));
    }
            resolve();
        }, 5000)
    });
}

export const Menu = () => {
  const dispatch = useDispatch();
  return <div className="Menu">
    {
      Object.values(ProductList).map(product => <ProductCard key={product.name} product={product} onSelect={() => dispatch(addProductThunk(product))} />
    }
</div>
}

Utilisez createAsyncThunk  pour notre store

Vous l’aurez sans doute deviné, Redux Toolkit nous amène un outil pour gérer nos thunks, createAsyncThunk  !

Cet outil, à l’instar de createAction  , va nous simplifier la manière de déclarer et d’ajouter nos thunks à nos slices.

Je vous propose donc de réécrire notre thunk d’ajout de produit en utilisant cette fois-ci createAsyncThunk  de RTK.

Dans notre fichier cartSlide.js  , on reporte notre fonction addProductThunk  .

Elle devient une fonction qui :

  • en premier paramètre, comme pour les actions, va prendre une clé pour identifier le thunk ;

  • en deuxième paramètre, en tant que fonction asynchrone, prend toujours en dernier paramètre le thunkApi  .

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { getListQuantityProductPerName } from "../../app/selectors";
import * as ProductList from '../../common/models';

export const addProductThunk = createAsyncThunk( 'cart/addProductThunk' , async (product, thunkApi) => {
})

Le thunkApi  nous donne accès à plusieurs choses : le dispatch  et le getState  du store, une fonction rejectWithValue  et une fonction fulfillWithValue  pour annuler ou valider l’action asynchrone, ainsi que d’autres paramètres tels que requestId  , signal  et extra  .

Nous simplifions notre fonction en veillant à dispatch  un produit dans la liste sélectionné via thunkApi.dispatch(cartSlice.actions.addProduct(product));  :

export const addProductThunk = createAsyncThunk( 'cart/addProductThunk' , async (product, thunkApi) => {
    // Ajout du produit au panier via le dispatch du store
    thunkApi.dispatch(cartSlice.actions.addProduct(product));
})

Selon le choix de l’utilisateur, nous retournons une Promise qui sera résolue ( resolve()  ), s’il valide l’ajout au panier de la promo, et rejetée ( reject()  ) dans le cas contraire.

Notre fonction addProductThunk devient donc :

export const addProductThunk = createAsyncThunk( 'cart/addProductThunk' , async (product, thunkApi) => {
    thunkApi.dispatch(cartSlice.actions.addProduct(product));
    // Promise retourner par notre thunk
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const state = thunkApi.getState();
            const numberProductPerName = getListQuantityProductPerName(state);
                const numberForSpecialOffer = numberProductPerName.find((item) => item.title === "Poulet Croquant")?.quantity;
             if (numberForSpecialOffer === 2) {
              if(window.confirm("Voulez-vous ajouter une troisième fois ce produit à moitié prix ?")) {
                       resolve();
                  } else {
                       reject();
                  }
             } else {
                reject();
             }
        }, 5000)
      });
})

Dans notre slice, nous ajoutons les cas d’actions qui nous intéressent via la propriété  `extraReducers`  :

  • builder.addCase(addProductThunk.rejected, …)   lorsque notre fonction asynchrone sera rejected  ;

  • builder.addCase(addProductThunk.fulfilled, …)   lorsqu’elle sera resolved  .

extraReducers: function(builder) {
    builder.addCase(addProductThunk.fulfilled, (state) => {
        const specialOffer = ProductList.PouletCroquant
            return [...state, {...specialOffer, price:    Math.round((ProductList.PouletCroquant.price / 2) * 100) / 100}]
  })
    builder.addCase(addProductThunk.rejected, (state) => {
      return [...state]
    })
}

Pour le premier cas, nous renvoyons le state non modifié. Pour le second, nous ajoutons un produit à moitié prix.

Retrouvez ci-dessous l’implémentation complète de ces modifications dans le fichier  features/card/cartSlice.js  :

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { getListQuantityProductPerName } from "../../app/selectors";
import * as ProductList from '../../common/models';

export const addProductThunk = createAsyncThunk( 'cart/addProductThunk' , async (product, thunkApi) => {
    // Ajout du produit au panier via le dispatch du store
    thunkApi.dispatch(cartSlice.actions.addProduct(product));
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const state = thunkApi.getState();
            const numberProductPerName = getListQuantityProductPerName(state);
                const numberForSpecialOffer = numberProductPerName.find((item) => item.title === "Poulet Croquant")?.quantity;
              if (numberForSpecialOffer === 2) {
               if(window.confirm("Voulez-vous ajouter une troisième fois ce produit à moitié prix ?")) {
                        resolve();
                    } else {
                        reject();
                    }
                } else {
                    reject();
                }
        }, 5000)
      });
})

export const cartSlice = createSlice({
    name: 'list',
    initialState: {},
    reducers: {
        addProduct: (currentState, action) => {
            const listWithNewProduct = [...currentState, action.payload]
            return listWithNewProduct
        },
        removeProduct: (currentState, action) => {
            const list = [...currentState].filter(
              (item, index) => index !== action.payload
            )
            return list
        },
        applyVoucher: (currentState, action) => {
            const withVoucherList = currentState.map(
    item => item.title === 'Super Crémeux' ? ({...item, price: action.payload.price}) : item
     )
     return withVoucherList
    },
  },
  extraReducers: function(builder) {
    builder.addCase(addProductThunk.fulfilled, (state) => {
        const specialOffer = ProductList.PouletCroquant
            return [...state, {...specialOffer, price:    Math.round((ProductList.PouletCroquant.price / 2) * 100) / 100}]
  })
    builder.addCase(addProductThunk.rejected, (state) => {
        return [...state]
    })
  }
})

N’oublions pas de modifier le fichier features/menu/Menu.js  et d’ajouter l’import de addProductThunk  en supprimant l’implémentation précédente :

import { addProductThunk } from '../cart/cartSlice';

À vous de jouer !

Notre application de commande de burger prend forme.

Dans la vie réelle, il arrive parfois que les commandes soient abandonnées en cours de route : un client qui préfère passer commande à la caisse, un autre qui passe au petit coin. Le client qui arrive quelques minutes plus tard aura besoin d’un panier vide. C'est-à-dire, pouvoir reprendre à zéro.

On peut donc imaginer une fonctionnalité de suppression de liste de commande lorsque la dernière action de l’utilisateur dépasse un certain temps, on va dire 2 minutes (120 secondes / 120 000 millisecondes).

Il faut donc pouvoir exécuter un thunk que vous pouvez appeler resetOrderThunk  , à créer, qui déclenchera au bout de 120 secondes la suppression des produits de la liste.

Utilisez notre thunk  addProductThunk  pour dispatch resetOrderThunk  .

Vous avez trouvé ? Découvrez la solution dans le screencast suivant :

En résumé

  • Utiliser l’approche asynchrone permet de pouvoir exécuter du JavaScript non bloquant.

  • Utiliser des middlewares avec Redux permet d'interagir avec les actions de façon indirecte.

  • Les thunks permettent de faire des actions sur le store de façon asynchrone.

  • createAsyncThunk  nous permet de créer simplement des thunks et de les ajouter au store.

Nous avons à présent les outils pour ajouter des fonctionnalités “connectées” à notre application. Passons de suite à l’utilisation de web services dans notre store.

Exemple de certificat de réussite
Exemple de certificat de réussite