Recently I’ve been learning a lot about React and it’s associated ecosystem. If you have any awareness of React, you’ll probably be aware that it’s primarily concerned with the View layer. As a result, if you’re looking to build anything beyond the most simple application you’ll need a (sane) way to manage state.
One solution to this is Redux which bills itself as a predictable state container for JavaScript apps. If you’re looking to get up to speed with Redux there’s a fairly steep learning curve, so I’ve tried to document a few things I’ve picked up on my own journey in the hope that it will help other folks.
Redux manages a single representation of your app “state”
The key to Redux is that it enforces a single source of truth for your app’s data. This single state container is called a Store. It might be helpful to think of it as a plain object. Instead of scattering state around your app, it’s all housed in a single location which is great as you have one place to inspect to discover things about your data’s current condition.
However, unlike similar state containers such as Backbone.Model, it has no getters or setters. This stops you from triggering arbitrary modifications to your global state which helps to increase the predictability of your code by reducing side-effects.
In fact the only way to update the data in your Store is to dispatch an “action”.
Actions trigger modifications to state
To change something in your Redux app you have to dispatch an action. An action is a simple object containing a “type” describing the kind of action and a “payload”containing the data for that action. For example:
{
type: ADD_FOOD,
food: {
// food data here
}
}
Actions are one of the more confusing aspects of Redux because on their own actions don’t really do anything. They are just plain old JavaScript objects. To have any effect you need to create the actions…
Actions are dispatched using Action Creators
To trigger actions you use action creators. These are simple functions, which as the name implies, create actions by returning them. For example:
function addFood({ food }) {
return {
type: ADD_FOOD,
food: food
}
}
The terms action and action creator are similar but they are different things so it’s important to get them clear in your mind as being distinct from each other.
Again – all changes to your app must be triggered by calling action creators which then dispatch actions. To update the Store (the global app state) we use Reducers.
Reducers describe how the State should change
A reducer describes how your app’s state should change in response to new data. It takes the current state, some new data and returns a new application state.
A typical app will have many reducers, each managing a specific portion of the state. By assigning reducers to be responsible only for a small piece of state we reduce their complexity making our app’s modifications easier to reason about.
When an action is dispatched, it flows through all the reducers which can then optionally update the Store’s state depending on the type of action and it’s payload.
Below we have an example of a Foods reducer which responds to the action created by the addFood action creator we demonstrated above.
function foods(state = [], action) {
switch (action.type) {
case 'ADD_FOOD':
return [...state, action.food ];
default:
return state;
}
}
In the reducer above, we check for the ADD_FOOD
action type and then return a new version of the “foods” piece of state by taking all the items from the current state (using the ES6 “spread” operator) and appending to them the new “food” from the action’s payload.
Reducers should be pure
It’s very important to remember when authoring Reducers, that they should be pure functions. This means that given the same inputs they should always return the same output. In other words they should be 100% predictable.
The upshot of this is that inside of a Reducer you must never modify the existing state directly or do things that could cause side effects (eg: API calls…etc).
Reducers combine to create the Store
Like much in Redux, Reducers on their own don’t do much. We have to combine them together into a root reducer using the combineReducers
helper.
const rootReducer = combineReducers({
foods: foodsReducer,
auth: authReducer,
});
Once you have this in place you can create your store using createStore(rootReducer)
helper, passing in your root reducer.
Summary (for now!)
I’m still learning more about Redux (and React) every day, so as I go I’ll be back to update this post with my thoughts and explanations in the hope that it will help me in the future and perhaps other people who are on their own React-Redux journey.