React/Redux TodoMVC, Part 3: State
• CommentsLast time we introduced React; this time we’ll introduce Redux!
It’s time to add application state. How about the TODO items? Seems like a decent place to start.
Before we can add our first piece of application state, though, there’s some boilerplate we need to write. It’s time to introduce a lot of new concepts, including Redux.
Please be patient with this step. It seems like a lot to get started, but trust me, once it’s all in place, adding to it is really simple and easy!
Actions and State
Actions and state are kind of related. Once you have both actions and state, then you can add actions or state individually. But when you’re first starting out, you really have to define actions and state.
So, in order to define our list of TODO items (as state), we also have to define an action to go along with it. An “Add TODO” action sounds like it would be a great fit!
Actions
Actions are just plain old objects. Our actions are going to follow the Flux Standard Action model, which means they’ll have a type
property (generally a string, since that makes debugging and serialization easier) and (for non-error actions) a payload
property. There are a couple other properties, but those are for more advanced scenarios; this simple app will only need type
and payload
.
Let’s define our “Add TODO” action! Well, it’ll need a type
first off. In real-world projects, it’s useful to define all the action types in a single file. So, that’s what I’ll do here (in actionTypes.jsx
):
export default {
ADD_TODO: 'ADD_TODO'
};
Next, the payload
. The data that travels as arguments of the ADD_TODO
action is the TODO item itself (a string). So the payload
can simply be a string in this case.
Action Creators
Now that we have a good understanding of the structure of our action, we can implement the action creator. There’s a helper library called redux-actions
that minimizes the repetitive code. With that, we can define our ADD_TODO
action (in actions/todoActions.jsx
):
import ActionTypes from '../actionTypes';
import { createAction } from 'redux-actions';
export default {
add: createAction(ActionTypes.ADD_TODO)
};
This is a bit dense… To clarify, we’re creating an object TodoActions
that has a method add
on it. This method takes a single parameter, which is the value of the action’s payload
.
So, the line:
add: createAction(ActionTypes.ADD_TODO)
is really the same as this:
add: createAction(ActionTypes.ADD_TODO, todoItem => todoItem)
which is the same as this:
add: todoItem => ({ type: ActionTypes.ADD_TODO, payload: todoItem })
In Redux, an Action Creator is just a function that creates an action.
State
The next step is to define some application state (our collection of todos), and define how the ADD_TODO
action affects that state. Presumably, it should just add the new item to our collection. The TODOs in our collection will need a couple properties: the actual TODO text (which I’ll call text
), and a flag indicating whether the TODO is completed (which I’ll call completed
).
This is where it gets a bit wild.
Let’s define the reducer first, using the react-actions
helper method handleActions
(in reducer.jsx
):
import { handleActions } from 'redux-actions';
import ActionTypes from './actionTypes';
export default handleActions({
[ActionTypes.ADD_TODO]: (state, action) => [...state, { completed: false, text: action.payload }]
}, []);
OK, so let’s break this down. I’m calling handleActions
with two parameters: an object containing the actual reducer method, and an empty array []
. That second parameter is just the initial state of our todo collection - it’s easier to have it be an empty collection rather than undefined
.
Now, let’s look at the object. I’m using the ES2015 computed property key syntax to define a function named ADD_TODO
, which is our actual reducer.
Our reducer takes two parameters: the existing state and the incoming action, and returns the new state (that is, what the state becomes after applying the action).
Important note: A reducer should never do anything else! It’s a mistake to have a reducer do any I/O, or modify the existing state in-place. It should only return the new state.
For the ADD_TODO
action, we just need to add a new TODO to our collection, setting completed
to false
and text
to the TODO text (which, for the ADD_TODO
action, is the action’s payload
):
(state, action) => [...state, { completed: false, text: action.payload }]
I’m using the ES2015 array spread operator to easily create a new array of TODOs that contains all the elements of the old array of TODOs, plus the new TODO that I create from the action.
There are a few competing patterns for writing reducers. ImmutableJS is one popular option for representing state, in spite of its serialization difficulties. However, in my projects so far, I just use regular JS objects, along with a helper plugin that double-checks my immutability in dev builds. I find this approach is sufficient for me.
The Store
So, we’ve got some basic application state, and we’ve got an action, and we’ve got a reducer that applies that action to our state. Good so far, but there’s a couple more pieces to hook up still.
First off, we need to tell Redux to actually create our store. Recall from my drawing of how Redux works, that it will create the store using our reducer (the shaded parts are mostly done for us):
In Redux, our store is a singleton, so I’m going to just expose it as a global object from store.jsx
:
import { createStore } from 'redux';
import reducers from './reducers';
export default createStore(reducers);
Quite literally, I’m taking our reducer from reducers.jsx
and passing it to Redux’s createStore
method. Very straightforward.
You might wonder why I bothered putting this in its own file. That will become clear in the next post, when I add debugging helpers in here.
The Views
The final piece to hook up is to provide our store to our view components. Right now, our view components don’t actually use the application state (that is, they’re still just displaying the boilerplate HTML, not our actual collection of TODO items). However, they will start displaying the application state soon enough!
We provide the store to our views using Redux’s Provider
. It looks like this (in main.jsx
):
import { Provider } from 'react-redux';
import store from './store';
.. // function Main() hasn't changed
window.onload = () => render(<Provider store={store}><Main/></Provider>, document.getElementById('app'));
That’s it for now! We give the Provider
element our store, and it provides a way for our view components to get at it.
Now we have an action (with a type and a creator), a reducer to apply that action to our state, a singleton store containing the application state, and the provider that allows our views to read the application state.
The main thing missing, of course, is something to actually trigger the action. We’ll add that in soon, though!
Source code at this revision - Live site at this revision (ignore the “startup flicker”; that’s just due to the way it’s hosted)