Utilisez les props dans les selectors
Créons un composant PlayerScore
qui affiche le score d’un joueur dans Tennis Score.
Pour récupérer les données nécessaires à l’affichage du score, nous allons de nouveau utiliser useSelector
.
Pourquoi ne pas envoyer directement les données en props depuis le composant App
?
Cela serait effectivement possible, mais cela ne correspond pas vraiment à la philosophie de Redux. La documentation officielle de Redux recommande de connecter le plus de composants possible, car cela améliore les performances.
Mais nous allons quand même utiliser les props ! En effet, le composant PlayerScore
a besoin de connaître le nom et l’identifiant du joueur dont il doit afficher le score. Il va donc recevoir une props playerId
et une props playerName
.
Nous allons ensuite utiliser la props playerId
dans une fonction de selector pour accéder au score du joueur :
import { useSelector } from "react-redux";
export function PlayerScore({ playerId, playerName }) {
// playerId est soit "player1" soit "player2"
// on l'utilise dans le selector pour accéder au score du joueur !
const score = useSelector((state) => state[playerId]);
return (
<div className="player-score">
<p>{playerName}</p>
<p>{score}</p>
</div>
);
}
Et voilà, chaque composant PlayerScore
récupère ainsi uniquement le score du joueur passé en props !
Jusqu'à maintenant, nous avons utilisé les selectors uniquement pour accéder à une portion du state. Mais les selectors peuvent faire bien plus que cela ! 😎
Faites des calculs dans les selectors
Reprenons notre composant PlayerScore
pour ajouter le mot “Avantage” lorsqu'un des joueurs a l’avantage.
Pour faire cela, on peut ajouter un useSelector
pour récupérer state.advantage
, puis vérifier dans le composant si playerId
correspond à ce state :
const advantage = useSelector((state) => state.advantage);
const hasAdvantage = advantage === playerId;
// si `hasAdvantage` est `true` on affiche le mot "Avantage"
Le code ci-dessus fonctionne, mais on peut faire mieux ! Au lieu d’extraire advantage
et de le comparer dans le composant, on peut faire la comparaison directement dans le selector :
const hasAdvantage = useSelector((state) => state.advantage === playerId);
Est-ce que cela fait vraiment une grosse différence ? J’ai l’impression que c’est presque pareil.
Dans le cas présent, c’est un peu vrai. Mais nous allons voir qu'il est possible d’écrire les selectors en dehors des composants React. Déplacer la logique dans les selectors devient alors beaucoup plus intéressant, car cela permet de simplifier nos composants et réutiliser la logique du selector !
Déclarez les selectors en dehors des composants
Lorsqu’un selector devient complexe ou que l’on a besoin de le réutiliser, on peut déplacer le selector en dehors du composant.
Cela permet également de donner un nom au selector, ce qui aide à la lecture du code.
Reprenons l’exemple du hasAdvantage
et voyons ce qu’il se passe si l'on déclare le selector en dehors du composant :
const selectPlayerHasAdvantage = (state) => state.advantage === playerId;
// Erreur ⛔ ️ la variable playerId n’existe pas !
export function PlayerScore({ playerId, playerName }) {
const hasAdvantage = useSelector(selectPlayerHasAdvantage);
// ...
}
La fonction selector
n’a plus accès à la variable playerId
! Pour remédier à cela, nous allons créer une fonction qui recevra en paramètre le playerId
et qui retournera la fonction de selector.
Autrement dit, on va créer une fonction qui retourne une fonction (un selector). Voici à quoi ressemble le code :
const selectPlayerHasAdvantage = (playerId) => {
return (state) => state.advantage === playerId;
}
On peut maintenant utiliser cette fonction dans notre composant, sans oublier de passer playerId
en paramètre !
const hasAdvantage = useSelector(selectPlayerHasAdvantage(playerId));
Comprenez ce qui provoque un render
Lorsque le state de Redux change, React-Redux réexécute les selectors, puis compare leur résultat avec le résultat précédent. Si le résultat est différent, alors le composant qui contient ce selector va être de nouveau rendu pour que les informations affichées soient toujours à jour.
Comme on l’a vu précédemment, useSelector
utilise l’opérateur ===
pour comparer les valeurs. C’est d’ailleurs pour cette raison qu’il ne faut pas changer le state directement. Il faut utiliser le destructuring ou Immer dans le reducer.
Mais que se passe-t-il si l’on crée une nouvelle référence dans un selector ? Par exemple, on pourrait créer un selector selectPlayersScore
qui sélectionne uniquement les scores des joueurs et les retourne dans un objet :
const selectPlayersScores = state => {
// ⛔️ Ce code n'est pas correcte
// Il ne faut pas créer de référence dans un reducer !
return {
player1: state.player1,
player2: state.player2,
}
}
Si vous avez bien suivi le chapitre précédent sur les données en JavaScript, vous avez peut-être déjà compris le problème. On crée une nouvelle référence à chaque exécution de selectPlayersScores
, et le résultat ne sera donc jamais égal au résultat précédent ! Même si les scores des joueurs n'ont pas changé, le composant qui utilise ce selector va être inutilement mis à jour.
Il faut donc faire bien attention de ne jamais créer des objets ou des tableaux dans les selectors. Dans le cas du selectPlayersScores
, il est préférable de faire deux selectors :
const selectPlayer1Score = state => state.player1;
const selectPlayer2Score = state => state.player2;
... ou bien de faire un selector qui attend un paramètre playerId
:
const selectPlayerScore = (playerId) => {
return (state) => state[playerId];
};
Voyons maintenant comment créer un composant PlayerScore
avec tout ce qu’on vient de voir :
Vous pouvez retrouver le code de ce screencast sur la branche P2C3-player-score du repository.
Exercez-vous
C’est maintenant à vous de créer le composant PlayerScore
. En partant de la correction du chapitre précédent, vous allez devoir :
Créer le composant
PlayerScore
et le connecter à Redux.Extraire les selectors dans un fichier
selectors.js
pour mieux organiser l’application.Créer un composant
PlayerPoints
qui affiche le nombre de jeux gagnés par chacun des joueurs (en utilisant la propriétéhistory
du state).
Une fois que vous avez terminé, allez jeter un œil à la version corrigée sur la branche P2C3-solution du repository.
Bonus : Saurez-vous afficher pour chaque joueur le nombre d’échanges consécutifs à gagner pour remporter le jeu ?
Par exemple si le score est de 15-40, le joueur 1 est à 4 échanges de la victoire alors que le joueur 2 est à 1 échange. Si le score est de 40-40, les deux joueurs sont à 2 échanges de la victoire.
Voici, à présent, le code de correction de l'exercice :
Vous pouvez retrouver le code du bonus sur la branche P2C3-bonus du repository.
En résumé
Un selector est une fonction que l’on passe à
useSelector
pour extraire un morceau du state.Il est possible d’utiliser les props dans les selectors, cela permet de faire des composants dynamiques tel que le composant
PlayerScore
.Pour simplifier les composants, on peut déclarer les selectors dans un fichier séparé ; si le composant utilise des props, il faut utiliser une fonction qui retourne le selector.
Il ne faut jamais créer de référence dans les selectors, car React-Redux va croire qu’ils changent à chaque changement de state.
Jusqu’à maintenant, nous avons uniquement manipulé des changements synchrones, c'est-à-dire immédiats. Dans le chapitre suivant, nous allons voir comment on peut gérer des événements asynchrones.