Note: This post is not about what Redux is or how to use Redux. This is written under the assumption that you have decided that Redux is a good fit for the problem you are trying to solve.
Spend time to design your state tree. The state tree is supposed to be the single source of truth in your application. If it is screwed, it is axiomatic that the rest of the app is screwed too.
Usually, a big chunk of the state tree is used to store resources (i.e. objects loaded from your backend server). I design the tree such that each class of resources is stored in a branch in the state tree and has the following shape:
{
[resourceName]: {
byId: {
[id1]: {
id: id1,
...otherProperties
},
[id2]: {
id: id2,
...otherProperties
}
},
lastFetched: new Date(),
isFetching: false,
...otherUsefulProps
}
}
There are three main properties:
byId
- This is an object that maps an instance's ID into the instance
itself. This representation might not be intuitive as usually the server
returns the list as an array. There is nothing wrong with using an array, but
using an object is much more convenient when we wish to, say, get an instance
with a particular ID. Each of the objects here should be denormalized, in
other words, in cases where they are associated to other objects, keep the
IDs to those objects instead of instances of them. Notice that they keys in
JavaScript objects are always strings, hence, if the IDs are numbers, they
will be coerced into string. It might be necessary to do property
type-casting in the other parts of the application.lastFetched
- This is set to the time when the last fetch operation is
performed. This is used to determine how fresh the data is. The application
can then proactively refetch from the server when has been a while since the
last fetch.isFetching
- Set to true when a fetch is in progress. This is useful to
show a spinner and prevent double fetch.We can add more properties to this subtree if it makes sense to do so.
Sometimes, it makes sense to keep track of some metadata on each object. For
example, we might want to know if we are loading a single object. In cases like
this, it is very convenient to have a, say, __meta__
property to keep track of
these metadata.
Make sure there is no way a state tree can conflict with itself. Sometimes, it
might be tempting to store some computed data in the state tree. For e.g., if we
are storing a list of messages, each message has an isRead
property. It might
be tempting to store an unreadCount
. The problem here is that now we need to
keep synchronize these two properties, and there is a non-zero probability of
things going out of sync. This is precisely the kind of problem that React and
Redux are trying to solve.
This brings us to the cases when the computation of the derived property is expensive. There are ways to solve this and will be discussed later.
Notice that reducers should never mutate the state argument:1
// Don't do this
const reducer = (state = {count: 0}, action) => {
switch (action.type) {
case 'INCREMENT':
state.count += 1;
return state;
case 'DECREMENT':
state.count -= 1;
return state;
default:
return state;
}
};
The idiomatic way is to always return a new instance of an object if anything changed:
const reducer = (state = {count: 0}, action) => {
switch (action.type) {
case 'INCREMENT':
return {
count: state.count + 1
};
case 'DECREMENT':
return {
count: state.count - 1
};
default:
return state;
}
};
This may or may not cause perf issues.2 If it does, you can consider using libraries that implement structural sharing such as Immutable. However, note that Immutable exposes a set of API that can be quite different from vanilla JS objects and arrays. So use this only if you really need it.
The designers of Redux explicitly mention that the Redux reducers should be
pure. As a result, it can be quite a bit of a hassle to initiate and manage
side-effects (such as loading things from the server). One of the simplest
solutions is redux-thunk. redux-thunk "allows you to write action
creators that return a function instead of an action". This returned function
takes in a dispatch
function and can dispatches real actions later on. (The
dispatch
function is the same as that from store.dispatch
. redux-thunk is
making use of dependency injection to decouple the action creators from the
store).
Action creators should always be a function even if they do not take in any arguments. For example:
// OK
const fetchPosts = () => (dispatch) => {
return fetch('/posts')
.then((response) => response.json())
.then((posts) => dispatch({
type: 'RECEIVE_POSTS',
posts
}));
};
const toggleMenu = () => ({
type: 'TOGGLE_MENU'
});
// Not OK
const fetchPosts = (dispatch) => {
// Same thing as before
};
const toggleMenu = {
type: 'TOGGLE_MENU'
};
There are two very good reasons:
Backward compatibility in the future - Suppose in the future we wish to add
optional params to the action creators (e.g., a page
params for
fetchPosts
when we decided to add pagination). This way, we can set a
default value for these params and the old code remain working:
const fetchPosts = (page = 1) => (dispatch) => {
// Do important things
};
// Places that call dispatch(fetchPosts()) continue to work without any
// modifications.
Action creators that are async should always return a promise (or use some async callback pattern if promise is not sufficient). This allows the composition of async action creators:
const someAction = () => (dispatch) => {
return dispatch(refreshFeed())
.then(() => dispatch(updateAnalytics()))
.then(() => dispatch(someOtherAction()));
};
To fully exploit the benefit of writing an SPA, we should do optimistic updates whenever possible. One major consideration when performing optimistic update is to do error handling - when 💩 hits the fan. There are a number of ways to do error handling in this context. Here, we assume that we will reverse the update and show an error message. We also need to keep in mind that multiple optimistic updates (of the same actions, such as liking a post) can happen at the same time. So we need a way to keep track of each of this actions. One way to do this is to tag each action with a unique ID. After that, we can swap this update with the real thing or cancel it, both based on this unique ID. For example,
const createPost = (title, content) => (dispatch) => {
const id = uuid();
dispatch(insertPost({ id, title, content }));
api.createPost(title, content)
.then((post) => {
dispatch(removePost(id)); // Remove the temp one
dispatch(insertPost(post)); // Insert the real one
// Note: This is done for the sake of simplicity and **might not** work
// for all cases. For example, your React component might be keyed using
// the post ID here, which will cause a new instance to be used. If this
// is a problem, there might be a need to have an internal ID. That will
// not be changed.
})
.catch((error) => {
dispatch(removePost(id));
// Somehow display error message
});
};
const reducer = (state = {}, action) => {
switch (action.type) {
case 'INSERT_POST':
return {
...state,
[action.post.id]: action.post
};
case 'REMOVE_POST':
const { [action.id]: garbage, ...newState } = state;
return newState;
default:
return state;
}
};
Because reducers are pure by default, they are really easy to test:
ES has some pretty nice syntax that can be used to updated a nested state object easily without mutation:
How do you update a nested object without mutating it? My answer: https://t.co/K8oknMHgfY pic.twitter.com/iVCIlIo7MR
— Dan Abramov (@dan_abramov) September 14, 2017
Quite a number of reducers are actually very straightforward and are repeated many times across the application. For example:
For cases like this, we can write higher-order reducers to generate these reducers. For example, this higher-order reducer can be used to toggle a boolean state when an action is dispatched:
const makeToggleReducer(initialState, actionType) => {
return (state = initialState, action) => {
switch (action.type) {
case actionType:
return !state;
default:
return state;
}
};
};
const reducer = combineReducers({
showSideMenu: makeToggleReducer(false, 'TOGGLE_SIDE_MENU')
});
As mentioned in a previous section, there should only be a single source of truth. This creates problem when other parts of the applications depend on information that is expensive (by some measure) to compute. The example I gave was calculating the number of unread messages from a list of messages.
The first course of action is to not do anything. Although CS education teaches us to not go for O(N) when you can do O(1), if it is not causing any perf issue, then just forget about it.3
However, if you have decided that this computation is indeed causing perf
issue, there is an easy fix - use reselect in your
mapStateToProps
. The idea behind reselect is that if the input to a function
does not change, it will just return the cached result, which is a valid
assumption for Redux as the reducers should always return a new state instead of
mutating it.
const { createSelector } from 'reselect';
const unreadCountSelector = createSelector(
(state) => state.messages,
(messages) => messages.reduce((count, {isRead}) => isRead ? count : count + 1, 0)
);
const mapStateToProps = (state) => ({
unreadCount: unreadCountSelector(state)
});
This way, if the state tree is updated, as long as state.messages
is
untouched, the selector function will just return the previous results.
When you are using React with Redux, setState
is really not an
anti-pattern.
When people say setState() is an anti-pattern, they mean 500-line components with a complex setState() soup scattered across event handlers.
— Dan Abramov (@dan_abramov) April 26, 2016
In fact, at times, setState
is a much more superior option. See, the Redux
state tree is essentially a global object. Although we do fancy things like DI
and HOC to decouple the store from everything else, the dependency is still
there (just that which store they depend on can be varied easily now). When we
use setState
instead of Redux dispatch, we are removing such dependency and
the component we are building becomes more standalone.
setState
is the more suitable option when the state is highly localized and is
not shared between components. For example, let's say we are implementing a
component for the user to select some images from a set of images. The images
are grouped (say in groups of four). There are also previous and next buttons to
move between groups. Now, it is obvious that the current page number is a state
that we have to keep track of somewhere. This state is something that other
components do not care about. Hence, it should go into local component state.
For this toy example, using Redux is actually a bad design. If we move the state into the Redux state tree, that means we need to write the action creators to move between pages, a reducer to update the page number, and a container to connect the component to the state and action creators. Now notice that the state is now written into a fixed location in the state tree. What happens if we want to use multiple instances of this component? (Not an unreasonable assumption.) Do we write another set of action creators, reducers, and containers?4
I find it useful to always start from using local component state and refactor the state out to the Redux store only when necessary. This greatly reduces the overall complexity of the application.
One of the practical reasons behind this is that the rest of the Redux ecosystem is making this assumption. So mutating the state will silently break you app. For example, reselect will wrongly return the cached results. ↩
Usually it doesn't. ↩
How do you know if it is causing perf issue? Always measure and/or experiment. ↩
Technically, this can be solved by accepting this location as a props and
referencing it in mapStateToProps
and/or mapDispatchToProps
: ↩
const mapStateToProps = (state, ownProps) => ({
page: state.gallery[ownProps.name].page
});
In fact, Redux Form is using such approach. However, we are really just moving the problem one level up - the parent component is now not reusable. (Or we can play the same trick and move it one level up again, but the problem will never go away.)