Nicks
Nicks

Reputation: 774

Redux, how to reuse reducer/actions?

I am trying to create an HOC or at least reduce the number of reducer/action. So I create one library-reducer that will hold all my data and one getItemList action to handle every action. When calling the action from react componentDidMount() I will pass a parameter like ( product, user, etc... ), this param will know which api and which state to update (ex: state.library.product).

I would like to have your advice about this technic, is it a good way ?

Thanks

const initialState = {
    contact: {
        tmp_state: { addresses: {} },
        item: null,
        receivedAt: null,
        isFetching: false,
        isError: false,
        list: []
    },
    expense: {
        tmp_state: {},
        item: null,
        receivedAt: null,
        isFetching: false,
        isError: false,
        list: []
    },
    service: {
        tmp_state: {},
        item: null,
        receivedAt: null,
        isFetching: false,
        isError: false,
        list: []
    },

    product: {
        tmp_state: {},
        item: null,
        receivedAt: null,
        isFetching: false,
        isError: false,
        list: []
    }
};

export default (state = initialState, action) => {

    // Init reducer name
    var name =  action.type.split("_").pop().toLowerCase();

    switch (action.type) {
        case `REQUEST_${name.toUpperCase()}`:
            return  { 
                ...state,
                [name]: {
                    ...state[name],
                    isFetching: true,
                },
            }

        case `FAILED_${name.toUpperCase()}`: 
            return {
                ...state,
                [name]: {
                    ...state[name],
                    isFetching: false,
                    isError: true,
                }
            }

        case `RECEIVE_${name.toUpperCase()}`:
            return  { 
                ...state,
                [name]: {
                    ...state[name],
                    isFetching: action.isFetching,
                    list: action.payload,
                    receivedAt: action.receivedAt
                }
            }

        case `GET_${name.toUpperCase()}`: 
            return {
                ...state,
                [name]: {
                    ...state[name],
                    item: action.item,
                    isFetching: action.isFetching,
                }
            }
        case `STATE_${name.toUpperCase()}`: 
            var fieldName = action.payload.fieldName.startsWith('_')
            if(fieldName){
                state[name].tmp_state.addresses = { ...state[name].tmp_state.addresses , [ action.payload.fieldName ] : action.payload.value }
            }else{
                state[name].tmp_state = { ...state[name].tmp_state, [ action.payload.fieldName ] : action.payload.value }
            }
            return {
                ...state,
                [name]: {
                    ...state[name]
                }
            }
            
        case `CREATE_${name.toUpperCase()}`:
            return {
                ...state,
                [name]: {
                    ...state[name],
                    isFetching: action.isFetching,
                    tmp_state: initialState[name].tmp_state,
                    list: [ ...state[name].list, action.item ]
                }
            }
        default:
            return state;
    }
}
  

// manager/src/redux/HOC/getListAction.js


import axios from 'axios';
import { API_ENDPOINT, apiCall } from '../../api/constant'
import { requestData, requestFailed  } from './'

// TMP DATA
// import contacts from '../../FAKE_DATA/contacts.json'

// GET FULL LIST OF CLIENT
export function getItemList( actionType ){

    return dispatch => {

        dispatch(requestData(actionType))

        axios.get(`${API_ENDPOINT}${apiCall(actionType).endPoints.get}`, {
          method: 'GET',
          mode: 'cors',
          headers: {
              'x-access-token': localStorage.getItem('token')
          }
        })
        .then(function (response) { 
            return response.data
        }) 
        .then( res => {
          if(res.success){
              dispatch(receiveItems(actionType, res.payload ))  
              }else{
                dispatch(requestFailed(actionType))
              }
        })              
    }
}

function receiveItems(actionType, items) {
  return {
    type: `RECEIVE_${actionType}`,
    payload: items,
    receivedAt: Date.now()
  }
}

Upvotes: 4

Views: 4759

Answers (2)

Rafael Rozon
Rafael Rozon

Reputation: 3019

Your code works and I think there's nothing wrong with it. I would do slightly different. I would wrap that reducer in a function and pass the name of state slice that the reducer will care about and the initial state, for example:

const makeReducer = (name, initialState) => (state = initialState, action) => {

    var actionType = name.toUpperCase();

    switch (action.type) {
        case `REQUEST_${actionType}`:
            return  { 
                ...state,
                [name]: {
                    ...state[name],
                    isFetching: true,
                },
            }
        // the rest, replace constants accordingly

}

Then the main reducer would be:

export default combineReducers({
      contact: makeReducer("contact", initialState.contact),
      expense: makeReducer("expense", initialState.expense),
      service: makeReducer("service", initialState.service),
      product: makeReducer("product", initialState.product)
});

You can use combineReducers in different cases to reuse reducer logic.

Check the redux docs: https://redux.js.org/recipes/structuring-reducers/reusing-reducer-logic/

Upvotes: 5

Buggy
Buggy

Reputation: 3649

Split reducer into baseReducer - reducer that we want to reuse and default - reducer that apply baseReducer to each slice of the state.

class BaseState {
  tmp_state = {};
  item = null;
  receivedAt = null;
  isFetching = false;
  isError = false;
  list = []
}

export const baseReducer = (state = new BaseState(), action) => {
  switch (action.payload.subtype) {
    case `REQUEST`:
      return {
        ...state,
        isFetching: true,
      }
    case `FAILED`:   /* code */
    case `RECEIVE`:  /* code */
    case `GET`:      /* code */
    case `STATE`:    /* code */
    case `CREATE`:   /* code */
    default:         /* code */

  }
}

class InitialState = {
  contact = new BaseState();
  expense = new BaseState();
  service = new BaseState();
  product = new BaseState();
}

export default (state = new InitialState(), action) => {
  switch (action.type) {
    case 'CONTACT':
      return {
        ...state,
        contact: baseReducer(state.contact, action)
      }
    case 'EXPENSE': /* the same */
    case 'SERVICE': /* the same */
    case 'PRODUCT': /* the same */
    default: return state;
  }
}

We can generalize further default reducer if we have a lot of item.

const smartCompose = mapActionTypeToState => (state, action) => {
  const stateSlice = mapActionTypeToState[action.type];
  if (!stateSlice) return state;

  return {
    ...state,
    [stateSlice]: baseReducer(state.contact, action),
  }
}

const mapActionTypeToState = {
  CONTACT: 'contact',
  EXPENSE: 'expense',
  SERVICE: 'service',
  PRODUCE: 'produce',
};

export const defaultReducer = smartCompose(mapActionTypeToState);

Upvotes: 4

Related Questions