In this step, we will look at solving the problems of complex applications (as mentioned in Step 4) with a library called Redux.
As a reminder, the problems that we want to address are:
Redux is an implementation of the Flux architectural pattern:
A view is a React component that consumes the store as its data.
Actions are serializable JSON messages that represent some event, such as a user's action or a network request. With the aid of reducers, they affect the overall state. At minimum, an action should contain a type
key. Sometimes it contains additional data as a payload.
The store consists of a state tree, a dispatcher, and reducers.
The state tree is a singleton, serializable, immutable nested JSON structure. It is updated from one snapshot to another using reducers.
The dispatcher accepts actions, passing them to the reducers.
Reducers are functions that take in the current state tree and an action, producing the next snapshot of the state tree. This is the only way to update the state tree.
There are lots of alternatives available, but here are some really good reasons to go with Redux:
The createStore()
function is provided by Redux to create a store. In general, an application has a single store. The function typically takes in the main reducer and an initial snapshot of the state tree.
const store = createStore(reducer, initialState);
We'll write our reducers with the help of some utilities from the official redux-starter-kit
, which greatly decreases the amount of boilerplate needed. The process for designing and implementing reducers is as follows:
Given a state tree shaped like this:
{
todos: {
[id: string]: TodoItem,
},
filter: 'all' | 'complete' | 'active'
}
We would organize our reducers matching the keys of the state tree and combine them with combineReducers()
:
import { createReducer } from 'redux-starter-kit';
import { combineReducers } from 'redux';
const reducer = combineReducers({
todos: createReducer({}, {
addTodo: (state, action) => { /* ... */ }
}),
filter: createReducer('all', {
setFilter: (state, action) => { /* ... */ }
})
})
In plain Redux, reducers must make a copy of the state before making modifications, but createReducer()
will automatically translate all the mutations to the state into immutable snapshots (!!!!!):
// first argument: initial state
// second argument: object whose keys correspond to possible values of action.type
const todosReducer = createReducer(
{},
{
addTodo: (state, action) => {
state[action.id] = { label: action.label, completed: false };
}
}
);
Dispatching an action will pass the action and the current state to the reducers. The root reducer will produce a new snapshot of the entire state tree. We can inspect the affected snapshot with the help of getState()
.
const store = createStore(reducer, initialState);
store.dispatch({ type: 'addTodo', label: 'hello' });
store.dispatch({ type: 'addTodo', label: 'world' });
console.log(store.getState());
Creating these action messages by hand is tedious, so we use action creators to do that:
const actions = {
addTodo = (label: string) => ({ label, id: nextId(), completed: false })
};
store.dispatch(actions.addTodo('hello'));
Nothing to show here; just look at your console window for output. Hit F12 (cmd+option+I
on Mac) to open console window.
To inspect Redux store, use the Redux Dev Tool extension for your browser: Chrome, FireFox. (Sorry, no Edge or IE support)