Reputation: 774
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
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
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