Como reduzir o número e aumentar a legibilidade do código em react-redux, redux-saga

Neste artigo, gostaria de compartilhar minha experiência de usar o pacote react-redux e redux-saga , ou melhor, qual “bicicleta” eu uso para reduzir a quantidade do mesmo tipo de código e simplificar sua percepção.

O que não combina comigo


As bibliotecas react-redux e redux-saga são simples, flexíveis e convenientes, mas possuem redundância de código. Os principais elementos são:

  1. Fábricas de eventos


    const actionCreatorFactory = type => payload => ({ type, payload });
    
    export const INITIALIZE = 'INITIALIZE';
    
    export const ON_FIELD_CHANGE = 'ON_FIELD_CHANGE';
    export const onFieldChange = actionCreatorFactory(ON_FIELD_CHANGE);
    
    export const HANDLE_FIELD = 'HANDLE_FIELD';
    export const handleField = actionCreatorFactory(HANDLE_FIELD);
    
    export const GO_TO_NEXT_STEP = 'GO_TO_NEXT_STEP';
    export const goToNextStep = actionCreatorFactory(GO_TO_NEXT_STEP);
    

    Como tal, várias coisas me confundem:

    - uma descrição dos tipos de eventos. Neste exemplo, você certamente pode ficar sem constante, mas ainda precisa passar seu tipo na fábrica, que será idêntico ao nome dos eventos criados na notificação camel ( camelCase ).

    - se você esqueceu a estrutura da carga , para recuperá-la, você precisa ir ao redutor / saga , onde este evento é usado, e ver o que precisa ser transferido para lá
  2. Redutores


    import getInitialState, {
      formDataInitialState as initialState,
    } from '../helpers/initialState';
    import { HANDLE_FIELD_DONE, ON_FIELD_CHANGE, RESET } from '../actionCreators';
    
    export default (state = initialState, { type, payload }) => {
      switch (type) {
        case RESET: {
          return getInitialState().formDataInitialState;
        }
    
        case ON_FIELD_CHANGE: {
          const { name } = payload;
          return {
            ...state,
            [name]: '',
          }
        }
    
        case HANDLE_FIELD_DONE: {
          const { name, value } = payload;
          return {
            ...state,
            [name]: value,
          }
        }
      }
    
      return state;
    };
    

    Aqui, em geral, apenas a construção do switch é irritante
  3. Sagas


    import { all, put, select, fork, takeEvery } from 'redux-saga/effects';
    import { runServerSideValidation } from '../actionCreators';
    import { HANDLE_FIELD } from '../actionCreators';
    
    function* takeHandleFieldAction() {
      yield takeEvery(HANDLE_FIELD, function*({ payload }) {
        const { validation, formData } = yield select(
          ({ validation, formData }) => ({
            validation: validation[payload.name],
            formData,
          })
        );
    
        const valueFromState = formData[payload.name];
    
        if (payload.value !== valueFromState) {
          const { name, value } = payload;
          const { validator } = validation.serverValidator;
          yield put(
            runServerSideValidation({
              name,
              value,
              validator,
              formData,
            })
          );
        }
      });
    }
    
    export default function* rootSaga() {
      yield all([fork(takeHandleFieldAction())]);
    }
    

    Nas sagas, o maior problema é a dificuldade de ler. Apenas um olhar superficial para entender o que torna a saga difícil. Uma razão para isso é o aninhamento profundo. Obviamente, você pode retirar os trabalhadores para reduzir a profundidade, mas a redundância aumenta.

    import { all, put, select, fork, takeEvery } from 'redux-saga/effects';
    import { runServerSideValidation } from '../actionCreators';
    import { HANDLE_FIELD } from '../actionCreators';
    
    function* takeHandleFieldWorker({ payload }) {
        const { validation, formData } = yield select(
          ({ validation, formData }) => ({
            validation: validation[payload.name],
            formData,
          })
        );
    
        const valueFromState = formData[payload.name];
    
        if (payload.value !== valueFromState) {
          const { name, value } = payload;
          const { validator } = validation.serverValidator;
          yield put(
            runServerSideValidation({
              name,
              value,
              validator,
              formData,
            })
          );
      }
    }
    
    function* takeHandleFieldWatcher() {
      yield takeEvery(HANDLE_FIELD, takeHandleFieldWorker);
    }
    
    export default function* rootSaga() {
      yield all([fork(takeHandleFieldWatcher())]);
    }
    

    Também existem verificações que aumentam o aninhamento ou adicionam pontos de saída da saga, o que piora o nosso código.

Como estou tentando resolver esses problemas


Vamos em ordem.

  1. Fábricas de eventos


    import { actionsCreator, payload } from 'sweet-redux-saga';
    
    @actionsCreator()
    class ActionsFactory {
      initialize;
      
      @payload('field', 'value')
      onFieldChange;
      
      @payload('field')
      handleField;
      
      @payload('nextStep')
      goToNextStep;
    }
    

    actionsCreator(). , (initialize;/onFieldChange;/handleField;/gpToNextStep;) action creator. , payload(...[fieldNames]). :

    class ActionsFactory {
      initialize = () => ({
        type: 'INITIALIZE',
        payload: undefined,
      });
    
      onFieldChange = (field, value) => ({
        type: 'ON_FIELD_CHANGE',
        payload: {
          field,
          value,
        },
      });
    
      handleField = field => ({
        type: 'HANDLE_FIELD',
        payload: {
          field,
        },
      });
    
      goToNextStep = nextStep => ({
        type: 'GO_TO_NEXT_STEP',
        payload: {
          nextStep,
        },
      });
    }
    

    toString, toPrimitive, valueOf. :

    const actionsFactory = new ActionsFactory();
    console.log(String(actionsFactory.onFieldChange)); // 'ON_FIELD_CHANGE'
    

  2. import getInitialState, {
      formDataInitialState as initialState,
    } from '../helpers/initialState';
    import { HANDLE_FIELD_DONE, ON_FIELD_CHANGE, RESET } from '../actionCreators';
    import { reducer } from '../../../leadforms-gen-v2/src/decorators/ReducerDecorator';
    
    @reducer(initialState)
    export class FormDataReducer {
      [RESET]() {
        return getInitialState().formDataInitialState;
      }
    
      [ON_FIELD_CHANGE](state, payload) {
        const { name } = payload;
        return {
          ...state,
          [name]: '',
        };
      }
    
      [HANDLE_FIELD_DONE](state, payload) {
        const { name, value } = payload;
        return {
          ...state,
          [name]: value,
        };
      }
    }
    

    reducer([initialState]). , , .

    function reducer(state = initialState, action) {
      if (!action) {
        return state;
      }
    
      const reducer = instance[action.type];
      if (reducer && typeof reducer === 'function') {
        return reducer(state, action.payload);
      }
    
      return state;
    }
    

  3. import { all, put, select, } from 'redux-saga/effects';
    import { runServerSideValidation } from '../actionCreators';
    import { HANDLE_FIELD } from '../actionCreators';
    import { sagas, takeEvery, filterActions } from 'sweet-redux-saga';
    
    @sagas()
    class MySagas {
      @takeEvery([HANDLE_FIELD])
      @filterActions(
        ({state, payload }) => state.formData[payload.name] === payload.value
      )
      * takeHandleFieldAction({ payload }) {
        const { validation, formData } = yield select(({ validation, formData }) => ({
          validation: validation[payload.name],
          formData,
        }));
    
        const { name, value } = payload;
        const { validator } = validation.serverValidator;
        yield put(
          runServerSideValidation({
            name,
            value,
            validator,
            formData,
          })
        );
      }
    }
    
    export default function* rootSaga() {
      yield all([
        new MySagas(),
      ]);
    }
    

    crie uma classe com anotação sagas (). Ao criar uma instância da classe, obtemos um gerador de funções que chama todos os campos da classe marcados com anotações takeEvery ([... [actionTypes]]) ou takeLatest ([... [actionTypes]]] em um thread separado:

    function* mySagas() {
      yield all([fork(mySagas.takeHandleFieldAction())]);
    }
    

    A anotação filterActions ({state, type, payload}) também pode ser usada com campos ; nesse caso, a saga será chamada apenas se a função retornar verdadeira .

Conclusão


Em geral, usando essas anotações simples, as sagas se tornaram mais claras e limpas, e a quantidade de código necessária para adicionar nova lógica foi reduzida. A navegação do projeto ficou mais fácil.

Fiz essas anotações no pacote sweet-redux-saga . Se houver outras soluções, terei prazer em compartilhar comigo.

All Articles