Organisation des réducteurs - aller plus loin


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, // List of our jedi data: [], error: undefined, } const reducerJedi = (state = reducerJediInitialState, action) => { switch (action.type) { case actionTypeJediCreateInit: return { ...state, loading: true, } case actionTypeJediCreateSuccess: return { loading: false, data: [...state.data, action.payload], error: undefined, } case actionTypeJediCreateError: return { ...state, loading: false, error: action.payload, } default: return state } } 

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) => { // Pick a reducer by action type const reducer = reducerJediMap[action.type] if (!reducer) { // Return state unchanged if we did not find a suitable reducer return state } // Run suitable reducer if found one return reducer(state, 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 { // Take a look at "Class field delcaratrions" proposal, which is now at Stage 3. // https://github.com/tc39/proposal-class-fields initialState = { loading: false, data: [], error: undefined, } @Action(actionTypeJediCreateInit) startLoading(state) { return { ...state, loading: true, } } @Action(actionTypeJediCreateSuccess) addNewJedi(state, action) { return { loading: false, data: [...state.data, action.payload], error: undefined, } } @Action(actionTypeJediCreateError) error(state, action) { return { ...state, loading: false, error: action.payload, } } } 

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) => { // Get method names from class' prototype const proto = Object.getPrototypeOf(instance) const methodNames = Object.getOwnPropertyNames(proto).filter( (name) => name !== 'constructor', ) // We want to get back a collection with action types and corresponding reducers const res = [] methodNames.forEach((methodName) => { const actionTypes = Reflect.getMetadata( METADATA_KEY_ACTION, instance, methodName, ) // We want to bind each method to class' instance not to lose `this` context const method = instance[methodName].bind(instance) // We might have many action types associated with a reducer actionTypes.forEach((actionType) => res.push({ actionType, method, }), ) }) return res } 

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.


 // We move that generic code to a dedicated module import { Action, createClassReducer } from 'utils/reducer-class' const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' class ReducerJedi { // Take a look at "Class field delcaratrions" proposal, which is now at Stage 3. // https://github.com/tc39/proposal-class-fields initialState = { loading: false, data: [], error: undefined, } @Action(actionTypeJediCreateInit) startLoading(state) { return { ...state, loading: true, } } @Action(actionTypeJediCreateSuccess) addNewJedi(state, action) { return { loading: false, data: [...state.data, action.payload], error: undefined, } } @Action(actionTypeJediCreateError) error(state, action) { return { ...state, loading: false, error: action.payload, } } } export const reducerJedi = createClassReducer(ReducerJedi) 

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.

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


All Articles