Organização do redutor através de uma classe padrão

Saudações, hoje eu vou falar com você sobre como o Redutor está organizado. E para dizer por onde comecei e para onde vim.


Portanto, existe um certo padrão para organizar o Reducer e fica assim:


export default function someReducer(state = initialState, action) { switch (action.type) { case 'SOME_REDUCER_LABEL': return action.data || {}; default: return state; } } 

Tudo é simples e claro aqui, mas, tendo trabalhado um pouco com essas construções, percebi que esse método tem várias dificuldades.


  • As tags precisam ser armazenadas de alguma forma, porque começaram a se arrastar pelo projeto e a rastrear muito além dos controladores.
  • As etiquetas precisavam ser únicas, porque, caso contrário, poderia haver uma interseção com outros redutores
  • Na maioria das vezes, ao trabalhar com essa estrutura, passava-se a organizar o código, em vez de processar os dados recebidos.
  • E quando há muitos rótulos no redutor, o código se torna desleixado e difícil de ler, e o espaço para nome geral não me agradou abertamente.
    Na mesma época, começamos a usar sagas para processar efeitos colaterais. Isso nos permitiu facilitar bastante a comunicação com o lado do servidor sem o uso de retornos de chamada.

Agora tínhamos que deixar a saga saber qual redutor precisava ser chamado depois que o efeito colateral funcionasse.


A opção mais sensata que encontrei é criar um criador de ações.


E nosso código anterior começou a ficar assim:


  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 é uma função geradora de ação (doravante denominada criador de ação) para uma saga que solicita dados de um servidor e os envia para um redutor, cujo rótulo foi passado para a função no estágio de inicialização (SOME_REDUCER_LABEL).


Agora, os rótulos do redutor foram exportados do redutor ou o criador da ação foi exportado do redutor para a saga e a padrão. Além disso, esse manipulador foi criado para cada rótulo. Isso só causou dor de cabeça, porque, depois que abri o redutor, contei 10 constantes dos rótulos definidores, algumas chamadas para vários criadores de ações para sagas e, em seguida, a função de processamento de estado do redutor, parecia algo assim


 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; } } 

Ao importar todas essas ações para o controlador, essa também foi inflada demais. E ficou no caminho.


Tendo examinado tantos redutores, imaginei que estamos escrevendo muito código de utilidade que nunca muda. Além disso, devemos garantir que enviamos o estado clonado ao componente.


Então tive a ideia de padronizar redutor. Tarefas diante dele não foram difíceis.


  1. Verifique a ação recebida e retorne o estado antigo se a ação não for para o redutor atual ou clone automaticamente o estado e passe-o para o método manipulador, que alterará o estado e o retornará ao componente.
  2. Você deve parar de usar rótulos. Em vez disso, o controlador deve receber um objeto contendo todos os criadores de ações para o redutor em que estamos interessados.
    Assim, depois de ter importado esse conjunto uma vez, posso executar vários criadores de ações para despachar funções do redutor para o controlador sem a necessidade de reimportar
  3. em vez de usar uma caixa de interrupção desajeitada com um espaço de nomes comum que o linter usa, quero ter um método separado para cada ação, no qual o estado clonado do redutor e a própria ação serão transferidos
  4. seria bom poder herdar um novo redutor do redutor. Caso a lógica se repita, mas, por exemplo, para um conjunto diferente de rótulos.

A ideia me pareceu viável e decidi tentar implementá-la.


Aqui está como o redutor médio começou a se parecer agora


  //    ,        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 dentro é o seguinte


 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; } 

Como fica no controlador? E assim


 import { someReducerInstActions } from '../../../SomeReducer.js' const mapDispatchToProps = dispatch => ({ doSoAction: (params) => dispatch(someReducerInstActions.fetch(url, params)), doSoAction1: (value, block) => dispatch(someReducerInstActions.update({value, block})), }); 

Então, o que temos como resultado:


  1. se livrou de acumular tags
  2. se livrou de um monte de importações no controlador
  3. caixa de interruptores removida
  4. eles venceram as sagas uma vez e agora podemos expandir seu set em um só lugar, garantindo que todos os herdeiros recebam automaticamente manipuladores adicionais de efeitos colaterais
  5. Temos a oportunidade de herdar dos redutores, caso haja lógica relacionada (no momento isso nunca foi útil para mim =))
  6. Eles mudaram a responsabilidade de clonar do desenvolvedor para uma classe que definitivamente se lembraria de fazê-lo.
  7. menos rotina ao criar redutor
  8. Cada método possui um espaço para nome isolado

Tentei descrever tudo com o máximo de detalhes possível =) Desculpe, se confuso, o Chukchi não é escritor. Espero que minha experiência seja útil para alguém.


O exemplo atual pode ser visto aqui


Obrigado pela leitura!


UPD: erros corrigidos. Ele escreveu à noite, mal leu. Obrigado por apontá-los tão delicadamente =)

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


All Articles