Understanding Redux
After using Redux on a handful of large applications, this is my understanding of how it should be used.
The simple example
const todos = (state = [], {type, payload}) => {
switch (type) {
case "ADD_TODO":
return [...state, payload];
default:
return state;
}
};
const store = createStore(todos);
store.dispatch({type: "ADD_TODO", payload: "Learn Redux"});
The whole state of your app is stored in an object tree inside a single store.
So what is “whole state” of this application? I think the first thing we need to understand is the difference between state and data. It’s very convenient to put everything into to the redux state, and derive the visible state from the Redux state.
const mapStateToProps = todos => ({
todos: todos.filter(activeTodos).take(5) // Take the top 5 todos
});
So we keep every single todo the user has ever created (potentially thousands), in memory locally on device, that does not sound like it’s going to end well.
I would argue that the user’s todo items are data and not application state. So what is application state? Let’s take a look at what is displayed on screen, and attempt to determine the application state that would be required to describe it.
- Which filter has been selected (All, Active, Completed)
- Text input
- Total number of items remaining
- Titles of the visible todos
That’s it if I were to inspect the Redux state for this application I should see.
const state = {
textInput: "Drink Coffee",
filter: "Active",
total: 2,
visableTodos: ["Learn Redux", "Write Article"]
};
It’s a good idea to think of its shape before writing any code. What’s the minimal representation of your app’s state as an object?
So to build this user interface we don’t need every todo the user has ever created, we just need the todos they are currently viewing. So where does the user data live? We solved this problem along time ago, data lives in a Database or Remote Server.
Redux is the minimum currently active application state and the database/server is the source of truth of all the user’s data. With this knowledge lets rewrite the todo example with an async action creator.
const addTodo = todo => async (dispatch, getState) => {
dispatch(resetTextInput());
await api.post("/todos", todo);
const {filter} = getState();
const result = await api.get(`/todos?filter=${filter}`);
dispatch(updateVisableTodos(result.todos));
dispatch(updateTotal(result.total));
};
When a user creates a new todo we send it to the server to be stored, and then we query the API to get the updated list of todos. If this was an offline application we would save it in localstorage. What happens when the user changes the filter?
const changeFilter = newFilter => async dispatch => {
dispatch(changeFilter(newFilter));
const {filter} = getState();
const result = await api.get(`/todos?filter=${filter}`);
dispatch(updateVisableTodos(result.todos));
dispatch(updateTotal(result.total));
};
I find most of the application logic lives in the async action creators, since reducers have to be pure and synchronous. I extract much of this logic into a repository.
const TodoRepository = {
addTodo: todo => api.post("/todos", todo),
getTodos: filter => api.get(`/todos?filter=${filter}`)
};
I challenge you to look at your Redux state and see how much state you have that is irrelevant to what the user is currently doing.
Other examples
- A book reading app, while the user is reading a book the application state is: Page number, total pages, font size, current page text. We don’t store every book the user has ever read or may read in Redux and every page of the book.
- A shopping app, while the user is searching for Coke, the application state is: The search term Coke, number of results, and the titles of results. We don’t store every product in the inventory in Redux nor the product details.