I’ve been working with Redux in different size projects for about 2 years already and during this time I tried and experimented on different things. I have come up with a lot of practices that helped me to rediscover Redux and feel good with it. Here are 7 tips following which would probably make you be happy as I do.
1. Follow FSA (Flux Standard Action) format
Flux standard action requires a redux action to have keys such as type
, payload
, meta
and error
. There must not be any other keys inside of an action. If an action is a failure, payload should contains an error object and error key is set to true.
Basically it makes providing static types for an action a lot easier. Additionally when using inside a reducer, you can use ES6 object destructuring once, extracting all the keys and use them wherever you need.
2. Use payload for putting data to store and meta as details about payload
Once you start following the first tip, you will probably have questions like: what should be on payload? when should I be using meta? which structure should both of them be? OK, here is what I’ve answered to these questions on my own.
First off about meta: it should include details about payload or some data from which you would be generating new state. You need to query your backend for one post, product or order? Pass unique key of what you need to get as a part of your meta. Here is an example:
{ type: 'POSTS/GET_ONE/START', meta: { key: 34 } }
Payload on the other hand should be the type of data that you would be updating your store with. Let’s say you are implementing searching functionality. Whenever user types in some information you trigger an action with the payload of what user has entered, so inside your reducer you directly put the data for the new state of it:
{ type: 'POSTS/SEARCH', payload: '<user typed value>' }
3. Always make your final reducer structure as an object
If your reducer is in a form of an array, string, boolean or number you are probably doing too much reducer combining or ruining namings of your data. If your code uses too many combineReducers
, then you are making lots of overhead, as combining does some calculations and comparing under the hood. Avoid doing like this:
const reducer = combineReducers({
searchText: searchTextReducer, // string
activeSorting: activeSortingReducer, // plain object { field: order } or null
})
Instead make them both one reducer and make it in form of an object:
const initial = {
searchText: '',
activeSorting: null,
}const reducer = (state = initial, { type, payload }) => {
if (type === 'SEARCH_TEXT_CHANGE') {
return { ...state, searchText: payload }
} return state
}
4. Use one action only inside of one reducer
This tip is a little bit controversial, because requires a little bit of performance regression, unless you use batch updates or batch dispatch actions. However it gives you a lot of DX, once you split your actions, so no reducers would share them, you can refactor it very easily and keep your actions close to your reducer and see what modifies your state and how.
5. Keep your business logic inside a middleware
If you’ve read the previous tip and feed confused about how to trigger update multiple reducer states when only one actions is dispatched, here is the answer for that question. Let’s say you have to get list of products from an API and you have a middleware function (in this case thunk, but the same works with saga and observable) where you fetch the data, here is how it should look like:
const fetchProducts = () => {
return (dispatch, getState) => {
const state = getState()
const searchText = state.products.ui.searchText
const params = {}
if (searchText !== '') {
params.q = searchText
}
dispatch({ type: 'PRODUCTS/FETCH_LIST/START' })
fetchMyData(API_URL, { params })
.then((response) => {
dispatch({ type: 'PRODUCTS/FETCH_LIST/FINISH' })
dispatch({ type: 'PRODUCTS/DATA/SET', payload: response })
})
}
}
Don’t pass any data to an action that you can collect from your store. Unload your components, reducers by moving all the business logic into your middlewares. If you are doing this inside of a component, then you are risking to make them fat and overwhelming, and also by connecting unnecessary data to your container you are making performance decrease.
6. Separate your store by concepts into data, state (for request states) and ui
What I see very often in other developers’ code is having a reducer structure like this:
{
isActive: false,
data: [],
error: null,
}
I found this pattern not very useful and you may have noticed on the previous example, I dispatched 2 different actions when fetch request has finished. That’s because I split my reducers into 3 pieces: data, state and ui. In data reducer I keep mostly the data came from backend and some additional pieces for optimistic adds/updates. State contains information about request states. Everything related to view layer like pages count, search text, filters, sorts, selected items, etc are stored in a UI reducer. This allows you to keep the data separately and modify it as you wish, not when a request has finished or started. Also delete requests or some manual action performing requests may not even return any data, so you don’t always have something that is coming back from server. Separating ui state gives a little bit of performance improvement, as it is modified quite often and if you keep all of them in a single reducer it may do some extra re-renders where you might not be expecting it. Here is how the structure of these reducers look like:
// data
{
list: {},
optimisticUpdates: {},
optimisticAdditions: {},
}
// state
{
addRequest: { isActive: false, error: null },
deleteRequests: [ { isActive: true, error: null, key: 2 } ],
updateRequests: [],
listRequest: { isActive: false, error: null },
singleRequests: [ { isActive: false, error: obj, key: 3 } ],
}
// ui
{
searchText: 'test',
optimisticDeleteKeys: [2],
sorting: { field: 'desc' },
pages: 5,
}
7. An async effect should have only start and finish actions
The last tip is pretty simple. It asks you to use only start and finish actions instead of start, success and fail actions. Once you use FSA, you may declare if it is a failure or not with an error key and inside of an action payload pass the error object itself. It will help you to reduce the amount of your action types and action creators, still allowing you to do whatever you were doing before.
I hope that you will hate Redux less from now on, if you start following these practices. Anything may become less painful if you do it the right way. Everything I wrote above is based on the outcomes of my own experience and you may be using different things and have no troubles as well.