
Qu'est-ce qu'on va couvrir ici?
Nous allons passer en revue l'évolution des réducteurs dans mes applications Redux / NGRX qui ont eu lieu au cours des deux dernières années. En partant du switch-case
de switch-case
vanille, en passant par la sélection d'un réducteur à partir d'un objet par clé, pour finalement régler avec des réducteurs basés sur une classe. Nous n'allons pas parler seulement de comment, mais aussi de pourquoi.
Si vous êtes intéressé à travailler avec trop de passe-partout dans Redux / NGRX, vous pouvez consulter cet article .
Si vous êtes déjà familier avec la sélection d'un réducteur à partir d'une technique de carte, pensez à passer directement aux réducteurs basés sur la classe .
Coffret interrupteur vanille
Jetons donc un œil à une tâche quotidienne de création d'une entité sur le serveur de manière asynchrone. Cette fois, je suggère de décrire comment créer un nouveau jedi.
const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' const reducerJediInitialState = { loading: false,
Soyons honnêtes, je n'ai jamais utilisé ce type de réducteurs en production. Mon raisonnement est triple:
switch-case
introduit certains points de tension, des tuyaux qui fuient, que nous pourrions oublier de réparer à temps à un moment donné. Nous pourrions toujours oublier de mettre en break
si nous ne faisons pas de return
immédiat, nous pourrions toujours oublier d'ajouter default
, que nous devons ajouter à chaque réducteur.switch-case
a lui-même du code switch-case
partout qui n'ajoute aucun contexte.switch-case
est O (n), en quelque sorte . Ce n'est pas un argument solide en soi car Redux n'est pas très performant de toute façon, mais cela rend fou mon perfectionniste intérieur.
La prochaine étape logique que la documentation officielle de Redux suggère de prendre est de choisir un réducteur à partir d'un objet par clé.
Sélection d'un réducteur à partir d'un objet par clé
L'idée est simple. Chaque transformation d'état est une fonction de l'état et de l'action et a un type d'action correspondant. Étant donné que chaque type d'action est une chaîne, nous pourrions créer un objet, où chaque clé est un type d'action et chaque valeur est une fonction qui transforme l'état (un réducteur). Ensuite, nous pourrions choisir un réducteur requis à partir de cet objet par clé, qui est O (1), lorsque nous recevons une nouvelle action.
const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' const reducerJediInitialState = { loading: false, data: [], error: undefined, } const reducerJediMap = { [actionTypeJediCreateInit]: (state) => ({ ...state, loading: true, }), [actionTypeJediCreateSuccess]: (state, action) => ({ loading: false, data: [...state.data, action.payload], error: undefined, }), [actionTypeJediCreateError]: (state, action) => ({ ...state, loading: false, error: action.payload, }), } const reducerJedi = (state = reducerJediInitialState, action) => {
Ce qui est cool ici, c'est que la logique à l'intérieur du reducerJedi
reste la même pour tout réducteur, ce qui signifie que nous pouvons le réutiliser. Il y a même une petite bibliothèque, appelée redux-create-reducer , qui fait exactement cela. Cela fait ressembler le code à ceci:
import { createReducer } from 'redux-create-reducer' const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' const reducerJediInitialState = { loading: false, data: [], error: undefined, } const reducerJedi = createReducer(reducerJediInitialState, { [actionTypeJediCreateInit]: (state) => ({ ...state, loading: true, }), [actionTypeJediCreateSuccess]: (state, action) => ({ loading: false, data: [...state.data, action.payload], error: undefined, }), [actionTypeJediCreateError]: (state, action) => ({ ...state, loading: false, error: action.payload, }), })
Joli et joli, hein? Bien que cette jolie ait encore quelques mises en garde:
- En cas de réducteurs complexes, nous devons laisser beaucoup de commentaires décrivant ce que fait ce réducteur et pourquoi.
- Les énormes cartes de réduction sont difficiles à lire.
- Chaque réducteur n'a qu'un seul type d'action correspondant. Que faire si je veux exécuter le même réducteur pour plusieurs actions?
Le réducteur basé sur la classe est devenu mon abri de lumière dans le royaume de la nuit.
Réducteurs basés sur la classe
Cette fois, permettez-moi de commencer par pourquoi de cette approche:
- Les méthodes de classe seront nos réducteurs et les méthodes ont des noms, ce qui est une méta-information utile, et nous pourrions abandonner les commentaires dans 90% des cas.
- Les méthodes de classe pourraient être décorées, ce qui est un moyen déclaratif facile à lire pour faire correspondre les actions et les réducteurs.
- Nous pourrions encore utiliser une carte d'actions sous le capot pour avoir la complexité O (1).
Si cela ressemble à une liste raisonnable de raisons pour vous, creusons!
Tout d'abord, je voudrais définir ce que nous voulons obtenir en conséquence.
const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' class ReducerJedi {
Maintenant que nous voyons où nous voulons aller, nous pourrions le faire étape par étape.
Étape 1. Décorateur d' action .
Ce que nous voulons faire ici, c'est accepter n'importe quel nombre de types d'actions et les stocker en tant que méta-informations pour une méthode de classe à utiliser plus tard. Pour ce faire, nous pourrions utiliser le polyfill Reflect - Metadata, qui apporte une fonctionnalité de métadonnées à l'objet Reflect . Après cela, ce décorateur attacherait simplement ses arguments (types d'action) à une méthode en tant que métadonnées.
const METADATA_KEY_ACTION = 'reducer-class-action-metadata' export const Action = (...actionTypes) => (target, propertyKey, descriptor) => { Reflect.defineMetadata(METADATA_KEY_ACTION, actionTypes, target, propertyKey) }
Étape 2. Création d'une fonction de réduction à partir d'une classe de réduction
Comme nous le savons, chaque réducteur est une fonction pure qui accepte un état et une action et renvoie un nouvel état. Eh bien, la classe est également une fonction, mais les classes ES6 ne peuvent pas être appelées sans new
et nous devons de toute façon créer un véritable réducteur à partir d'une classe avec quelques méthodes. Nous devons donc en quelque sorte le transformer.
Nous avons besoin d'une fonction qui prendrait notre classe, parcourrait chaque méthode, collecterait des métadonnées avec les types d'action, construirait une carte de réducteur et créerait un réducteur final à partir de cette carte de réducteur.
Voici comment nous pourrions examiner chaque méthode d'une classe.
const getReducerClassMethodsWthActionTypes = (instance) => {
Maintenant, nous voulons traiter la collection reçue dans une carte de réduction.
const getReducerMap = (methodsWithActionTypes) => methodsWithActionTypes.reduce((reducerMap, { method, actionType }) => { reducerMap[actionType] = method return reducerMap }, {})
Ainsi, la fonction finale pourrait ressembler à ceci.
import { createReducer } from 'redux-create-reducer' const createClassReducer = (ReducerClass) => { const reducerClass = new ReducerClass() const methodsWithActionTypes = getReducerClassMethodsWthActionTypes( reducerClass, ) const reducerMap = getReducerMap(methodsWithActionTypes) const initialState = reducerClass.initialState const reducer = createReducer(initialState, reducerMap) return reducer }
Et nous pourrions l'appliquer à notre classe ReducerJedi
comme ceci.
const reducerJedi = createClassReducer(ReducerJedi)
Étape 3. Fusionner le tout ensemble.
Prochaines étapes
Voici ce que nous avons manqué:
- Et si la même action correspond à plusieurs méthodes? La logique actuelle ne gère pas cela.
- Pourrions-nous ajouter immer ?
- Et si j'utilise des actions basées sur les classes? Comment pourrais-je passer un créateur d'action, pas un type d'action?
Tout cela avec des exemples de code et des exemples supplémentaires est couvert par la classe reducer .
Je dois dire que l'utilisation de classes pour les réducteurs n'est pas une pensée originale. @amcdnl a proposé des actions ngrx impressionnantes il y a un certain temps, mais il semble qu'il se concentre désormais sur NGXS , sans oublier que je voulais un typage et un découplage plus stricts de la logique angulaire. Voici une liste des principales différences entre la classe de réducteur et les actions ngrx.
Si vous aimez l'idée d'utiliser des classes pour vos réducteurs, vous aimerez peut-être faire de même pour vos créateurs d'action. Jetez un œil à la classe flux-action .
J'espère que vous avez trouvé quelque chose d'utile pour votre projet. N'hésitez pas à me faire part de vos retours! J'apprécie très certainement toute critique et question.