Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid repetitive code in redux (ducks approach)?

I've been working with React and Redux for about 3 years. Also I use redux-thunk for asynchronous stuff.

And I love them a lot, but recently I noticed that almost all ducks in my project are using the same structure of actions, reducers, selectors, etc.

For example - you have an application and it has some users and transactions (or similar) lists, item details and edit functionality. All of these lists or items have their own ducks (actions, reducers, selectors, etc).

Code below will show the problem more clearly:

// ACTIONS

const const setUser = user => ({
  type: types.SET_USER,
  payload: user,
});

const cleanUser = () => ({ type: types.CLEAN_USER });

const fetchUser = userId => dispatch =>
  dispatch(fetchApi(userRequests.get(userId)))
    .then(response => dispatch(setUser(response)))
    .catch(error => showNotification(error));

// delete, update, etc... user actions

// REDUCER

const userReducer = (state = null, action) => {
  switch (action.type) {
    case types.SET_GROUP_ITEM:
      return action.payload;
    case types.CLEAN_GROUP_ITEM:
      return null;
    default:
      return state;
  }
};

Code above shows the structure of user from users duck which will be almost the same for other ducks.

Is there any ways to reduce the repetitive code? Thank you for advance!

like image 276
Arsenowitch Avatar asked May 18 '18 14:05

Arsenowitch


1 Answers

I noticed that almost all ducks in my project are using the same structure of actions, reducers, selectors, etc.

I never implemented the reducks structure within Redux, but I did at one point find myself generating identical actions, reducers, etc. when managing my domain entities (e.g. Persons, Orders, Products, etc).

For instance, I always seemed to care about:

  1. Are we currently fetching the entity? isFetching
  2. Were there any errors fetching the entity? error
  3. What's the entity's actual data? data
  4. When was the entity last fetched? lastUpdated

Also, domain entities are getting added all the time, so continually copying and pasting the reducer/actions is not ideal. We need a way to dynamically store data in Redux, and we want that data to always be attached to properties like isFetching and lastUpdated.

{
  "entities": {
    <SOME_ENTITY>: {
      "isFetching" : null    // Am I fetching?
      "lastUpdated": null    // When was I last fetched?
      "data"       : null    // Here's my data!
      "error"      : null    // Error during fetching
    }
  }
}

So what if we issued an action with a string literal that will be used as a key within Redux (e.g. products, orders)? That way, we can issue whatever valid action types are available to us (FETCH_REQUEST, etc), and we simply need to update the entity key, which will automatically carve out the space in the Store for us:

dispatch({
    entity     : "products",
    type       : "FETCH_SUCCESS", 
    data       : [{id: 1}],
    lastUpdated: Date.now()
});

dispatch({
    entity    : "orders",
    type      : "FETCH_SUCCESS",
    data      : [{id: 2}, {id: 3}],
    lastUpdated: Date.now()
});

Resulting State

{
  "entities": {
    "products": {
      "isFetching" : false,
      "lastUpdated": 1526746314736,
      "data"       : [{id: 1}]
      "error"      : null
    },
    "orders": {
      "isFetching" : false,
      "lastUpdated": 1526746314943,
      "data"       : [{id: 2}, {id: 3}]
      "error"      : null
    }
  }
}

Generic Entities Reducer

function entities (state = {}, action) {
    switch (action.type) {
        case FETCH_SUCCESS: // fall through
        case FETCH_FAILURE: // fall through
        case FETCH_REQUEST: {
            return Object.assign({}, state, {
                [action.entity]: entity(
                    state[action.entity],
                    action
                )
            });
        }
        default: {
            return state;
        }
    }
};

Entity Reducer

const INITIAL_ENTITY_STATE = {
    isFetching : false,
    lastUpdated: null,
    data       : null,
    error      : null
};

function entity (state = INITIAL_ENTITY_STATE, action) {
    switch (action.type) {
        case FETCH_REQUEST: {
            return Object.assign({}, state, {
                isFetching: true,
                error     : null
            });
        }
        case FETCH_SUCCESS: {
            return Object.assign({}, state, {
                isFetching : false,
                lastUpdated: action.lastUpdated,
                data       : action.data,
                error      : null
            });
        }
        case FETCH_FAILURE: {
            return Object.assign({}, state, {
                isFetching : false,
                lastUpdated: action.lastUpdated,
                data       : null,
                error      : action.error
            });
        }
    }
}

Again, by using a generic reducer, we can dynamically store whatever we'd like into Redux, since we're using the entity string below as the key within Redux

dispatch({type: "FETCH_REQUEST", entity: "foo"});
dispatch({type: "FETCH_REQUEST", entity: "bar"});
dispatch({type: "FETCH_REQUEST", entity: "baz"});

Resulting State

{
  "entities": {
    "foo": {
      "isFetching": true,
      "error": null,
      "lastUpdated": null,
      "data": null
    },
    "bar": {
      "isFetching": true,
      "error": null,
      "lastUpdated": null,
      "data": null
    },
    "baz": {
      "isFetching": false,
      "error": null,
      "lastUpdated": null,
      "data": null
    }
  }
}

If this looks interesting, I did write a small lib (plug!) that does exactly what's described above:

  • https://github.com/mikechabot/redux-entity
  • https://www.npmjs.com/package/redux-entity

Live Demo: http://mikechabot.github.io/react-boilerplate/dist/

That said, I'm not pushing that lib whatsoever, I'm just trying to describe the approach I took given the problem I had. Your action set might be totally different, in which case, you can still implement the generic pattern, but obviously have the reducer will behave differently.

like image 151
lux Avatar answered Oct 31 '22 22:10

lux