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!
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:
isFetching
error
data
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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With