
De quoi s'agit-il?
Regardons les métamorphoses des réducteurs dans mes applications Redux / NGRX au cours des deux dernières années. Commençant par le switch-case
chêne, continuant avec la sélection de l'objet par clé et se terminant par des classes avec décorateurs, blackjack et TypeScript. Nous allons essayer de revoir non seulement l'histoire de cette voie, mais aussi de trouver une relation causale.
Si vous et moi posons des questions sur l'élimination d'un passe-partout dans Redux / NGRX, cet article peut vous intéresser.
Si vous utilisez déjà l'approche pour sélectionner un réducteur à partir d'un objet par clé et que vous en avez marre, vous pouvez immédiatement basculer vers "Réducteurs basés sur la classe".
Coffret chocolat
Habituellement, le switch-case
vanille, mais il m'a semblé que cela discriminait sérieusement tous les autres types de switch-case
de switch-case
.
Examinons donc un problème typique de création asynchrone d'une entité, par exemple un 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,
Je serai très franc et j'avouerai que je n'ai jamais utilisé de switch-case
dans ma pratique. Je voudrais croire que j'ai même une liste de raisons à cela:
switch-case
trop facile à casser: vous pouvez oublier d'insérer break
, vous pouvez oublier le default
.switch-case
trop verbeux.switch-case
presque O (n). Ce n'est pas très important en soi, car Redux ne se vante pas de performances à couper le souffle en soi, mais ce fait exaspère mon connaisseur intérieur de beauté.
La façon logique de combiner tout cela est offerte par la documentation officielle de Redux - pour choisir un réducteur dans l'objet par clé.
Sélection d'un réducteur à partir d'un objet par clé
L'idée est simple - chaque changement d'état peut être décrit par une fonction d'état et d'action, et chacune de ces fonctions a une certaine clé (champ de type
dans l'action) qui lui correspond. Parce que type
est une chaîne, rien ne nous empêche de trouver un objet pour toutes ces fonctions, où la clé est type
et la valeur est une fonction de conversion à l'état pur (réducteur). Dans ce cas, on peut choisir le réducteur nécessaire par touche (O (1)), lorsqu'une nouvelle action arrive au réducteur racine.
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) => {
La chose la plus délicieuse est que la logique à l'intérieur de reducerJedi
reste la même pour n'importe quel réducteur, et nous pouvons la réutiliser. Il existe même une nano-bibliothèque redux-create-reducer pour cela .
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, }), })
Il semble que rien ne se soit produit. Certes, une cuillerée de miel n'est pas sans baril de goudron:
- Pour les réducteurs complexes, nous devons laisser des commentaires, car cette méthode ne permet pas de sortir de la boîte pour fournir des méta-informations explicatives.
- Les objets avec un tas de réducteurs et de clés ne sont pas bien lus.
- Chaque réducteur n'a qu'une seule clé. Mais que se passe-t-il si vous souhaitez exécuter le même réducteur pour plusieurs jeux d'action?
J'ai failli fondre en larmes de bonheur lorsque je suis passé à des réducteurs basés sur la classe, et ci-dessous, j'expliquerai pourquoi.
Réducteurs basés sur la classe
Petits pains:
- Les méthodes de classe sont nos réducteurs et les méthodes ont des noms. Juste les informations très méta qui disent ce que fait ce réducteur.
- Les méthodes de classe peuvent être décorées, ce qui est un moyen déclaratif simple de lier les réducteurs et leurs actions correspondantes (à savoir, les actions, pas seulement une action!)
- Sous le capot, vous pouvez utiliser tous les mêmes objets pour obtenir O (1).
En fin de compte, je voudrais obtenir quelque chose comme ça.
const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' class ReducerJedi {
Je vois le but, je ne vois aucun obstacle.
Étape 1. Décorateur @Action
.
Nous avons besoin que dans ce décorateur, nous puissions coller n'importe quel nombre de jeux d'action, et que ces services soient enregistrés sous forme de méta-informations, qui seront accessibles ultérieurement. Pour ce faire, nous pouvons utiliser les merveilleuses métadonnées de réflexion polyfill, qui corrigent Reflect .
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. Transformez la classe en un réducteur.
Dessinez un cercle, dessinez une seconde, et maintenant un peu de magie et obtenez un hibou!
Comme nous le savons, chaque réducteur est une fonction pure qui prend l'état et l'action actuels et renvoie un nouvel état. Une classe est, bien sûr, une fonction, mais pas exactement celle dont nous avons besoin, et les classes ES6 ne peuvent pas être appelées sans new
. En général, nous devons en quelque sorte le transformer.
Donc, nous avons besoin d'une fonction qui prendra la classe actuelle, passera par chacune de ses méthodes, collectera des méta-informations avec des types d'actions, collectera un objet avec des réducteurs et créera le réducteur final à partir de cet objet.
Commençons par collecter des méta-informations.
const getReducerClassMethodsWthActionTypes = (instance) => {
Maintenant, nous pouvons convertir la collection résultante en objet
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 }
Ensuite, nous pouvons l'appliquer à notre classe ReducerJedi
.
const reducerJedi = createClassReducer(ReducerJedi)
Étape 3. Nous regardons ce qui s'est passé en conséquence.
Comment vivre?
Quelque chose que nous avons laissé dans les coulisses:
- Que faire si le même type d'action correspond à plusieurs réducteurs?
- Ce serait formidable d'ajouter immer hors de la boîte.
- Et si nous voulons utiliser des classes pour créer nos actions? Ou des fonctions (créateurs d'actions)? J'aimerais que le décorateur puisse accepter non seulement les types d'actions, mais aussi les créateurs d'actions.
Une petite bibliothèque de classe réducteur possède toutes ces fonctionnalités avec des exemples supplémentaires.
Il convient de noter que l'idée d'utiliser des classes pour les réducteurs n'est pas nouvelle. @amcdnl a déjà créé une excellente bibliothèque d' actions ngrx , mais il semble qu'il l'ait désormais notée et soit passée à NGXS . De plus, je voulais un typage plus rigoureux et réinitialiser le ballast sous forme de fonctionnalités spécifiques à Angular. Voici une liste des principales différences entre la classe de réducteur et les actions ngrx.
Si vous avez aimé l'idée de classes pour les réducteurs, vous aimerez peut-être aussi utiliser des classes pour vos actions. Jetez un œil à la classe flux-action .
J'espère que vous n'avez pas perdu de temps en vain, et l'article vous a au moins été un peu utile. Veuillez donner un coup de pied et critiquer. Nous apprendrons à mieux coder ensemble.