Bonjour, aujourd'hui, je vais vous parler de l'organisation de Reducer. Et pour dire où j'ai commencé et où je suis arrivé.
Donc, il existe une certaine norme pour organiser Reducer et cela ressemble à ceci:
export default function someReducer(state = initialState, action) { switch (action.type) { case 'SOME_REDUCER_LABEL': return action.data || {}; default: return state; } }
Tout est simple et clair ici, mais après avoir un peu travaillé avec de telles constructions, j'ai réalisé que cette méthode a un certain nombre de difficultés.
- Les balises doivent être stockées d'une manière ou d'une autre, car elles ont commencé à se déplacer le long du projet et à explorer bien au-delà des contrôleurs.
- Les étiquettes devaient être rendues uniques, car sinon il pourrait y avoir une intersection avec d'autres réducteurs
- La plupart du temps, lorsque l'on travaillait avec une telle structure, on passait à organiser le code plutôt qu'à traiter les données entrantes
- Et quand il y a beaucoup d'étiquettes dans le réducteur, le code devient bâclé et difficile à lire, et l'espace de noms général ne m'a pas ouvertement plu.
Vers la même époque, nous avons commencé à utiliser des sagas pour traiter les effets secondaires. Cela nous a permis de faciliter grandement la communication avec le côté serveur sans utiliser de rappels.
Maintenant, nous devions faire savoir à la saga quel réducteur devait être appelé après que l'effet secondaire se soit produit.
L'option la plus sensée que j'ai trouvée est de créer un créateur d'action.
Et notre code précédent a commencé à ressembler à ceci:
import { FetchSaga } from '../../helpers/sagasHelpers'; const SOME_REDUCER_LABEL = 'SOME_REDUCER_LABEL'; export const someReducerLabelActionCreator = FetchSaga.bind(this, SOME_REDUCER_LABEL); export default function someReducer(state = initialState, action) { switch (action.type) { case SOME_REDUCER_LABEL: return action.data || {}; default: return state; } }
FetchSaga est une fonction de générateur d'actions (ci-après dénommée créateur d'actions) pour une saga qui demande des données à un serveur et les envoie à un réducteur, dont l'étiquette a été transmise à la fonction au stade de l'initialisation (SOME_REDUCER_LABEL).
Désormais, les libellés du réducteur ont été exportés depuis le réducteur ou le créateur d'actions a été exporté depuis le réducteur pour la saga et la version standard. De plus, un tel gestionnaire a été créé pour chaque étiquette. Cela n'a fait qu'ajouter un mal de tête, car une fois que j'ai ouvert le réducteur, j'ai compté 10 constantes des étiquettes de définition, puis quelques appels à divers créateurs d'actions pour les sagas, puis la fonction de traitement d'état du réducteur, cela ressemblait à quelque chose comme ça
import { FetchSaga } from '../../helpers/sagasHelpers'; const SOME_REDUCER_LABEL1 = 'SOME_REDUCER_LABEL1'; .... const SOME_REDUCER_LABEL10 = 'SOME_REDUCER_LABEL10'; export const someReducerLabelActionCreator1 = FetchSaga.bind(this, SOME_REDUCER_LABEL1); ..... export const someReducerLabelActionCreator10 = FetchSaga.bind(this, SOME_REDUCER_LABEL10); export default function someReducer(state = initialState, action) { switch (action.type) { case SOME_REDUCER_LABEL: return action.data || {}; case SOME_REDUCER_LABEL1: return action.data || {}; case SOME_REDUCER_LABEL2: return action.data || {}; case SOME_REDUCER_LABEL3: return action.data || {}; .... default: return state; } }
Lors de l'importation de toutes ces actions dans le contrôleur, celle-ci était également tellement gonflée. Et ça a gêné.
Après avoir regardé tant de réducteurs, je me suis dit que nous écrivons beaucoup de code utilitaire qui ne change jamais. De plus, nous devons nous assurer que nous envoyons l'état cloné au composant.
Puis j'ai eu l'idée de standardiser le réducteur. Les tâches devant lui n'étaient pas difficiles.
- Vérifiez l'action entrante et retournez l'ancien état si l'action n'est pas pour le réducteur actuel ou l'état de clonage automatique et passez-le à la méthode du gestionnaire, qui changera l'état et le renverra au composant.
- Vous devez arrêter d'utiliser des étiquettes, à la place, le contrôleur devrait recevoir un objet contenant tous les créateurs d'actions pour le réducteur qui nous intéresse.
Ainsi, après avoir importé un tel ensemble une fois, je peux lancer à travers lui un certain nombre de créateurs d'actions pour envoyer des fonctions du réducteur au contrôleur sans avoir à réimporter - au lieu d'utiliser un commutateur maladroit avec un espace de noms commun que le linter utilise, je veux avoir une méthode distincte pour chaque action, dans laquelle l'état cloné du réducteur et l'action elle-même seront transférés
- ce serait bien de pouvoir hériter d'un nouveau réducteur de réducteur. Dans le cas où la logique se répète, mais par exemple pour un ensemble d'étiquettes différent.
L'idée m'a semblé viable et j'ai décidé d'essayer de la mettre en œuvre.
Voici à quoi ressemble le réducteur moyen maintenant
// , reducer' import stdReducerClass from '../../../helpers/reducer_helpers/stdReducer'; class SomeReducer extends stdReducerClass { constructor() { super(); /** reducer'. reducer action, */ this.prefix = 'SOME_REDUCER__'; } /** , reducer - type - , . - , action creator, SOME_REDUCE__FETCH. type action creator someReduceInstActions - method - , action, - sagas - , , . , action creator , SOME_REDUCE__FETCH, , , reducer . */ config = () => [ { type: 'fetch', method: this.fetch, saga: 'fetch' }, { type: 'update', method: this.update }, ]; // action creators init = () => this.subscribeReduceOnActions(this.config()); // , fetch = (clone, action) => { // return clone; }; // , update = (clone, action) => { // return clone; }; } const someReducerInst = new SomeReducer(); someReducerInst.init(); // action creators config // action creator export const someReducerInstActions = someReducerInst.getActionCreators(); // . checkActionForState Action , reducer' export default someReducerInst.checkActionForState;
stdReducerClass de l'intérieur est comme suit
import { cloneDeep } from 'lodash'; // lodash // , import { FetchSaga } from '../helpers/sagasHelpers/actions'; export default class StdReducer { _actions = {}; actionCreators = {}; /** UNIQUE PREFIX BLOCK START */ /** , . , , , reducer action */ uniquePrefix = ''; set prefix(value) { const lowedValue = value ? value.toLowerCase() : ''; this.uniquePrefix = lowedValue; } get prefix() { return this.uniquePrefix; } /** INITIAL STATE BLOCK START */ /** initialState reducer'. */ initialStateValues = {}; set initialState(value) { this.initialStateValues = value; } get initialState() { return this.initialStateValues; } /** PUBLIC BLOCK START */ /** * init() . , Config, action creator _subscribeAction * actionsConfig - , {type, method, saga?} , action creator */ subscribeReducerOnActions = actionsConfig => actionsConfig.forEach(this._subscribeAction); /** _subscribeAction, , type. , reducer , action. */ _subscribeAction = (action) => { const type = action.type.toLowerCase(); this._actions[type] = action.method; // this.actionCreators[type] = this._subscribeActionCreator(type, action.saga); // action creator } /** _subscribeActionCreator - , action creator - saga , - fetch , type , , action creator, , SOME_Reducer__, FETCH, SOME_Reducer__FETCH, action creator */ _subscribeActionCreator = (type, creatorType) => { const label = (this.prefix + type).toUpperCase(); switch (creatorType) { case 'fetch': return this._getFetchSaga(label); default: return this._getActionCreator(label); } } /** _getFetchSaga - , */ _getFetchSaga = label => FetchSaga.bind(this, label); /** _getActionCreator - action creator, , , . */ _getActionCreator = label => (params = {}) => ({ type: label, ...params }); /** , playload. action , */ checkActionForState = (state = this.initialState || {}, action) => { if (!action.type) return state; const type = action.type.toLowerCase(); const prefix = this.prefix; , , . const internalType = type.replace(prefix, ''); // if (this._actions[internalType]) { // - const clone = cloneDeep(state); // , , action , // return this._actions[internalType](clone, action); } // , action . return state; } /** action creator, reducer */ getActionCreators = () => this.actionCreators; }
À quoi cela ressemble-t-il dans le contrôleur? Et donc
import { someReducerInstActions } from '../../../SomeReducer.js' const mapDispatchToProps = dispatch => ({ doSoAction: (params) => dispatch(someReducerInstActions.fetch(url, params)), doSoAction1: (value, block) => dispatch(someReducerInstActions.update({value, block})), });
Alors, qu'avons-nous en conséquence:
- se débarrasser d'empiler des balises
- s'est débarrassé d'un tas d'importations dans le contrôleur
- boîtier de commutateur retiré
- ils ont battu les sagas une fois et maintenant nous pouvons étendre leur ensemble en un seul endroit, en étant sûr que tous les héritiers recevront automatiquement des gestionnaires d'effets secondaires supplémentaires
- Nous avons eu l'occasion d'hériter de réducteurs, au cas où il y aurait une logique connexe (pour le moment cela ne m'a jamais été utile =))
- Ils ont transféré la responsabilité du clonage du développeur à une classe qui se souviendrait certainement de le faire.
- moins de routine lors de la création du réducteur
- Chaque méthode a un espace de noms isolé
J'ai essayé de tout décrire le plus en détail possible =) Désolé, s'il est confus, les Tchouktches ne sont pas écrivains. J'espère que mon expérience sera utile à quelqu'un.
→ L'exemple actuel peut être vu ici
Merci d'avoir lu!
UPD: erreurs corrigées. Il a écrit la nuit, l'a mal lu. Merci de les avoir si délicatement pointés =)