Découvrez les sélecteurs
Prenons le temps de revenir sur nos progrès et arrêtons-nous un peu sur la façon dont nous accédons aux valeurs du store.
Dans le cas de la liste des produits sélectionnés, nous répétons chaque fois que nécessaire store.getState().list
.
Afin de bien respecter les principes du DRY (Don’t Repeat Yourself), nous allons regrouper nos accès. Ils seront ainsi disponibles directement via des imports. Nous avons donc besoin de déplacer l’accès à la liste, avoir le total de commande et savoir si oui ou non la promotion est utilisable.
Créons un fichier app/selectors.js
dans lequel nous allons mettre nos accès au store que nous appellerons à présent, vous l’avez deviné : des sélecteurs.
Nous créons donc une fonction getProductList
qui prend le state en paramètre et nous retourne list
, qui est la liste des produits sélectionnés.
En deuxième temps, nous ajoutons une fonction getTotalOrder
qui prend le state et nous retourne le montant total de la commande.
Enfin, nous ajoutons une fonction qui nous retourne true
s’il est possible d’appliquer la promotion, false
dans le cas contraire, nous la nommons isVoucherAvailable
:
export const getProductList = (state) => state?.list
export const getTotalOrder = (state) => getProductList(state).reduce((prv, cur) => cur.price + prv, 0)
export const isVoucherAvailable = (state) => getProductList(state).find((product) => product.title === "Super Crémeux")
Puis, nous mettons à jour les trois fichiers suivants : features/cart/Cart.js
,
import { useStore } from "react-redux";
import { SuperCremeux } from "../../common/models";
import { useEffect, useState } from "react";
import { getProductList } from '../../app/selectors'
export const Cart = () => {
const store = useStore();
const [list, setList] = useState(getProductList(store.getState()))
useEffect(() => {
store.subscribe(() => setList(getProductList(store.getState())))
})
return (
<div className="Selection">
<h1>Choisir son menu</h1>
<div className="CartNavBar">
<button
onClick={() =>
store.dispatch({ type: "ADD_PRODUCT", payload: SuperCremeux })
}
>
Ajouter un super crémeux
</button>
</div>
{list?.map((item, index) => (
<span key={index} className="SelectedProduct">
{item.title} {item.price} €
</span>
))}
</div>
);
};
features/cart/Total.js
,
import { useStore } from "react-redux";
import { useEffect, useState } from "react";
import { getProductList, getTotalOrder } from "../../app/selectors";
export const Total = () => {
const store = useStore();
const [list, setList] = useState(getProductList(store.getState()))
const totalCommand = getTotalOrder(store.getState())
useEffect(() => {
store.subscribe(() => setList(getProductList(store.getState())))
})
return <div className="TotalCommand">
{list.length === 0 ? <div>Aucun produit sélectionné</div> : <div>Total commande {totalCommand} euros</div>}
</div>
};
etfeatures/cart/Voucher.js
.
import { useStore } from "react-redux";
import { useEffect, useState } from "react";
import { isVoucherAvailable } from "../../app/selectors";
export const Voucher = () => {
const store = useStore();
const [available, setAvailable] = useState(isVoucherAvailable(store.getState()));
useEffect(() => {
store.subscribe(() => setAvailable(isVoucherAvailable(store.getState())));
});
return (
<div className="Voucher">
{available && (
<button
onClick={() =>
store.dispatch({ type: "APPLY_VOUCHER", payload: { price: 2 } })
}
>
Appliquer ma promo Super crémeux à 2 euros
</button>
)}
</div>
);
};
Les sélecteurs nous permettent de partager les implémentations logiques. Ce qui nous permet de rendre réutilisables nos accès au store.
Nous venons de revisiter notre implémentation en cherchant à simplifier. Mais nous répétons malgré tout les useEffect
. N’aurait-on pas un outil pour aller encore plus loin dans cette simplification ?
Eh si, nous voyons juste après un outil qui va nous permettre de gérer l’accès aux valeurs du store et de “re-rendre” les composants à tout changement.
Utilisez useSelector
OK, très bien, on avance à grands pas. Continuons à apporter des améliorations à notre code base en exploitant encore plus d'outils par react-redux.
Nous avons précédemment utilisé le hook useEffect
pour attacher le state local de chacun de nos composants aux changements du store.
Peut-on simplifier cette implémentation ? La réponse est oui, et nous allons tout de suite mettre en pratique la solution : useSelector
.
Ce hook nous permet de réaliser deux choses essentielles pour accéder aux valeurs à jour de notre store. Il récupère l’état complet du store. Nous pouvons donc accéder à toutes les valeurs.
De plus, il s’abonne aux changements d’état du store. Nous pouvons ainsi nous passer d’attacher un état local puisque le hook va exécuter notre sélecteur passé en paramètre à chaque changement d’état du store. Ce qui nous assure un rerender en cas de changement de valeur.
Pour l’utiliser, rien de plus simple.
Pour notre liste de produits, nous remplaçons notre constante list
et le state local par une constante list à laquelle nous assignons la valeur de useSelector(getProductList)
comme suit :
const list = useSelector(getProductList)
Je vous propose de suivre le screencast dédié :
Notre implémentation est beaucoup plus simple à présent. Nous avons remplacé nos useEffect
et setState
par un simple useSelector
dans nos fichiers :
features/cart/Cart.js
import { useSelector, useStore } from "react-redux";
import { SuperCremeux } from "./models";
import { getProductList } from "./selectors";
export const Cart = () => {
const store = useStore();
const list = useSelector(getProductList)
return <div className="Selection">
<h1>Choisir son menu</h1>
<div className="CartNavBar">
<button onClick={() => store.dispatch({type: 'ADD_PRODUCT', payload: SuperCremeux})}>Ajouter un super crémeux</button>
</div>
{
list.map(
(item, index) => <span className="SelectedProduct">{item.title} {item.price} €</span>
)
}
</div>
};
features/voucher/Voucher.js
import { useSelector, useStore } from "react-redux";
import { isVoucherAvailable } from "../selectors";
export const Voucher = () => {
const store = useStore();
const available = useSelector(isVoucherAvailable)
return <div className="Voucher">
{available && <button onClick={() => store.dispatch({type: 'APPLY_VOUCHER', payload: { price: 2 }})}>Appliquer ma promo Super crémeux à 2 euros</button>}
</div>
};
features/total/Total.js
import { useSelector } from "react-redux";
import { getProductList, getTotalOrder } from "../selectors";
export const Total = () => {
const list = useSelector(getProductList)
const totalCommand = useSelector(getTotalOrder)
return <div className="TotalCommand">
{list.length === 0 ? <div>Aucun produit sélectionné</div> : <div>Total commande {totalCommand} euros</div>}
</div>
};
À vous de jouer
À ce point d’étape, je vous propose d’améliorer notre affichage.
Deux choses à réaliser :
Afficher la liste des produits disponibles à la commande. Pour cela, rien de plus simple, je vous fournis le composant !
Simplifier la commande en regroupant les produits par type.
Mettons en application les deux cas suivants :
1- En tant qu’utilisateur,
je peux lister les produits affichés grâce au composant ProductCard que je vous fournis, dans un composant
features/menu/Menu.js
que nous ajoutons au composantapp/App.js
;je sélectionne un produit en cliquant sur un composant ProductCard. La props
onSelect
permet de déclencher un évènement au clic sur le composant ProductCard.
2- En tant qu’utilisateur,
je peux lister les produits de la commande regroupés par nom ;
je peux donc voir indiqué : X x Nom de produit / ie 5 x Super Crémeux ;
les produits affichés ont une quantité supérieure à 0.
Pour commencer, copiez les illustrations de nos burgers dans un dossier public/images
. Vous retrouverez les illustrations dans ce fichier du repository GitHub.
Ci-dessous le code du composant ProductCard :
const IMAGES = {
"Double Cantal": "images/DoubleCantal.svg",
"Super Crémeux": "images/SuperCremeux.svg",
"Poulet Croquant": "images/PouletCroquant.svg",
}
export const ProductCard = ({ product, onSelect }) => <div className="ProductCard" onClick={onSelect}>
<img src={IMAGES[product.title]} alt={product.title} />
{product.title}
</div>
Terminé ? Dans le screencast suivant, je vous montre ma solution :
En résumé
Utiliser les selectors nous permet de réécrire les accès au state dans chacun de nos composants.
useSelector
est un hook qui permet à la fois d’utiliser nos selectors et de connecter le composant aux changements du store.
Nous arrivons à faire pas mal de choses maintenant que nous maîtrisons nos accès au store. L’organisation de nos selectors et du store à cette échelle est tout à fait maintenable.
Nous allons par la suite exploiter les outils Redux Toolkit pour être capables de faire monter nos applications en complexité sans perdre de maintenabilité.
Mais avant de passer à cette troisième partie du cours, un quiz vous attend pour vous tester sur les 3 derniers chapitres. C’est parti !