Nous utilisons trop de sélecteurs redux

Lorsque je regarde le fichier {domain} /selectors.js dans les grands projets React / Redux avec lesquels je travaille, je vois souvent une énorme liste de sélecteurs redux de ce type:


getUsers(state) getUser(id)(state) getUserId(id)(state) getUserFirstName(id)(state) getUserLastName(id)(state) getUserEmailSelector(id)(state) getUserFullName(id)(state) … 

À première vue, l'utilisation de sélecteurs ne semble pas inhabituelle, mais avec l'expérience, nous commençons à comprendre qu'il peut y avoir trop de sélecteurs. Et il semble que nous ayons survécu jusqu'à ce point.


image

Redux et sélecteurs


Regardons Redux. Qu'est-ce qu'il est, pourquoi? Après avoir lu redux.js.org, nous comprenons que Redux est un "conteneur prévisible pour stocker l'état de l'application JavaScript"


Lors de l'utilisation de Redux, il est conseillé d'utiliser des sélecteurs, même s'ils sont facultatifs. Les sélecteurs ne sont que des getters pour obtenir certaines parties de l'état entier, c'est-à-dire fonctions de la forme (State) => SubState . Habituellement, nous écrivons des sélecteurs afin de ne pas accéder directement à l'état, puis nous pouvons combiner ou mémoriser les résultats de ces sélecteurs. Cela semble raisonnable.


Profondément immergé dans les sélecteurs


La liste de sélecteurs que j'ai citée dans l'introduction de cet article est caractéristique du code créé à la hâte.


Imaginez que nous ayons un modèle utilisateur et que nous voulons lui ajouter un nouveau champ de messagerie. Nous avons un composant qui s'attendait à ce que firstName et lastName soient entrés, et maintenant il attendra un autre email . En suivant la logique du code avec les sélecteurs, en introduisant un nouveau champ email, l'auteur doit ajouter le sélecteur getUserEmailSelector et l'utiliser pour passer ce champ au composant. Bingo!


Mais c'est le bingo? Et si nous obtenons un autre sélecteur, lequel sera plus compliqué? Nous allons le combiner avec d'autres sélecteurs, et peut-être arriverons-nous à cette image:


 const getUsers = (state) => state.users; const getUser = (id) => (state) => getUsers(state)[id]; const getUserEmailSelector = (id) => (state) => getUser(id)(state).email; 

La première question se pose: que doit getUserEmailSelector sélecteur getUserEmailSelector si le sélecteur getUser renvoie undefined ? Et c'est une situation probable - bugs, refactoring, héritage - tout peut y conduire. D'une manière générale, ce n'est jamais la tâche des sélecteurs de gérer les erreurs ou de fournir des valeurs par défaut.


Le deuxième problème se pose avec le test de tels sélecteurs. Si nous voulons les couvrir de tests unitaires, nous aurons besoin de données simulées identiques aux données de production. Nous devrons utiliser les données factices de l'état entier (car l'état ne peut pas être non cohérent en production) pour ce sélecteur uniquement. Cela, en fonction de l'architecture de notre application, peut être très gênant - faire glisser les données dans les tests.


Supposons que nous avons écrit et testé le sélecteur getUserEmailSelector comme décrit ci-dessus. Nous l'utilisons et connectons le composant à l'état:


 const mapStateToProps = (state, ownProps) => ({ firstName: getUserFirstName(ownProps.userId)(state), lastName: getUserLastName(ownProps.userId)(state), //   email: getUserEmailName(ownProps.userId)(state), }) 

En suivant la logique ci-dessus, nous avons obtenu ce groupe de sélecteurs qui étaient au début de l'article.
Nous sommes allés trop loin. En conséquence, nous avons écrit une pseudo-API pour l'entité Utilisateur. Cette API ne peut pas être utilisée en dehors du contexte de Redux car elle nécessite une conversion d'état complète. De plus, cette API est difficile à étendre - lors de l'ajout de nouveaux champs à l'entité Utilisateur, nous devons créer de nouveaux sélecteurs, les ajouter à mapStateToProps, écrire plus de code passe-partout.


Ou peut-être devriez-vous accéder directement aux champs d'entité?


Si le problème est seulement que nous avons trop de sélecteurs - peut-être que nous utilisons simplement getUser et accédons directement aux propriétés d'entité dont nous avons besoin?


 const user = getUser(id)(state); const email = user.email; 

Cette approche résout le problème de l'écriture et de la prise en charge d'un grand nombre de sélecteurs, mais elle crée un autre problème. Si nous devons changer le modèle utilisateur, nous devrons également contrôler tous les endroits où user.email trouve user.email ( note du traducteur ou autre champ que nous changeons). Avec une grande quantité de code dans le projet, cela peut devenir une tâche difficile et compliquer même un peu de refactoring. Lorsque nous avions un sélecteur, il nous protégeait de telles conséquences des changements, car a assumé la responsabilité de travailler avec le modèle et le code utilisant le sélecteur ne savait rien du modèle.


L'accès direct est compréhensible. Mais qu'en est-il de la réception des données calculées? Par exemple, avec le nom d'utilisateur complet, qui est une concaténation du prénom et du nom? Besoin de creuser plus loin ...


image

Le modèle de domaine est dirigé. Redux - Secondaire


Vous pouvez venir sur cette photo en répondant à deux questions:


  • Comment définissons-nous notre modèle de domaine?
  • Comment allons-nous stocker les données? (gestion des états, pour cela nous utilisons redux * note du traducteur * que la couche de persistance est appelée en DDD)

Répondant à la question «Comment définissons-nous le modèle de domaine» (dans notre cas, Utilisateur), abstenons-nous de redux et décidons ce qu'est un «utilisateur» et quelle API est nécessaire pour interagir avec lui?


 // api.ts type User = { id: string, firstName: string, lastName: string, email: string, ... } const getFirstName = (user: User) => user.firstName; const getLastName = (user: User) => user.lastName; const getFullName = (user: User) => `${user.firstName} ${user.lastName}`; const getEmail = (user: User) => user.email; ... const createUser = (id: string, firstName: string, ...) => User; 

Ce sera bien si nous utilisons toujours cette API et considérons le modèle utilisateur inaccessible en dehors du fichier api.ts. Cela signifie que nous ne nous tournerons jamais directement vers les champs de l'entité puisque le code qui utilise l'API ne sait même pas quelle entité a des champs.


Maintenant, nous pouvons revenir à Redux et résoudre les problèmes liés uniquement à l'état:


  • Quelle place occupent les utilisateurs dans notre article?
  • Comment stocker les utilisateurs? Une liste? Dictionnaire (valeur-clé)? Sinon, comment?
  • Comment allons-nous obtenir une instance d'utilisateur de l'État? La mémorisation doit-elle être utilisée? (dans le contexte du sélecteur getUser)

Petite API avec de grands avantages


En appliquant le principe du partage des responsabilités entre le domaine et l'État, nous obtenons de nombreux bonus.


Un modèle de domaine bien documenté (modèle utilisateur et son API) dans le fichier api.ts. Il se prête bien aux tests, car n'a pas de dépendances. Nous pouvons extraire le modèle et l'API dans la bibliothèque pour les réutiliser dans d'autres applications.


Nous pouvons facilement combiner des fonctions API comme sélecteurs, ce qui est un avantage incomparable par rapport à l'accès direct aux propriétés. De plus, notre interface de données est désormais facile à entretenir à l'avenir - nous pouvons facilement changer le modèle utilisateur sans changer le code qui l'utilise.


Aucune magie ne s'est produite avec l'API, cela semble toujours clair. L'API ressemble à ce qui a été fait à l'aide de sélecteurs, mais elle a une différence clé: elle n'a pas besoin de l'état complet, elle n'a plus besoin de prendre en charge l'état complet de l'application pour les tests - l'API n'a rien à voir avec Redux et son code passe-partout.


Les accessoires des composants sont devenus plus propres. Au lieu d'attendre que les propriétés firstName, lastName et email soient entrées, le composant reçoit une instance User et utilise en interne son API pour accéder aux données nécessaires. Il s'avère que nous n'avons besoin que d'un seul sélecteur - getUser.


Il existe des avantages pour les réducteurs et les middleware d'une telle API. L'essentiel de l'avantage est que vous pouvez d'abord obtenir une instance de User, traiter les valeurs manquantes, traiter ou éviter toutes les erreurs, puis utiliser les méthodes API. C'est mieux que d'utiliser l'obtention de chaque champ individuel à l'aide de sélecteurs isolés du domaine. Ainsi, Redux devient vraiment un «conteneur prévisible» et cesse d'être un objet «divin» avec une connaissance de tout.


Conclusion


Avec de bonnes intentions (lire ici - sélecteurs), la route de l'enfer est pavée: nous ne voulions pas accéder directement aux champs de l'entité et avons fait des sélecteurs séparés pour cela.


Bien que l'idée des sélecteurs lui-même soit bonne, sa surutilisation rend difficile le maintien de notre code.


La solution décrite dans l'article propose de résoudre le problème en deux étapes - décrire d'abord le modèle de domaine et son API, puis traiter avec Redux (stockage de données, sélecteurs). De cette façon, vous écrirez du code meilleur et plus petit - vous n'avez besoin que d'un sélecteur pour créer une API plus flexible et évolutive.


Notes du traducteur


  1. J'ai utilisé le mot état, car il semble qu'il soit fermement entré dans le vocabulaire des développeurs russophones.
  2. L'auteur utilise les mots en amont / en aval pour signifier "code de haut niveau / bas niveau" (si selon Martin) ou "code utilisé ci-dessous / code ci-dessous qui utilise ce qui est écrit ci-dessus", mais il n'est pas correct de comprendre comment l'utiliser dans la traduction Je pourrais donc me consoler en essayant de ne pas déranger le sens général.

J'accepterai volontiers les commentaires et suggestions de corrections dans PM et les corrigerai.

Source: https://habr.com/ru/post/fr485238/


All Articles