×
By the end of this chapter, you should be able to:
Redux is a centralized state management store. While it is commonly used with React, it is an entirely separate library that can be used on its own or with a other frameworks.
Here's an overview of the purpose of redux, courtesy of their documentation:
Redux architecture revolves around a strict unidirectional data flow.
This means that all data in an application follows the same lifecycle pattern, making the logic of your app more predictable and easier to understand. It also encourages data normalization, so that you don't end up with multiple, independent copies of the same data that are unaware of one another.
The data lifecycle in any Redux app follows these 4 steps:
- You call
store.dispatch(action)
.- The Redux store calls the reducer function you gave it.
- The root reducer may combine the output of multiple reducers into a single state tree.
- The Redux store saves the complete state tree returned by the root reducer.
Redux revolves around some core concepts in functional programming. Let's examine these in more detail before we talk about reducers.
A pure function is a predictable function that does not have any side-effects. What does that mean? Side effects occur when your function modifies the external environment somehow. Consider the following foo
function:
var myObj = {}; function setObject(a, b) { myObj[a] = b; } // all of these modify the same object setObject('firstNumber', 55); // { firstNumber: 55 } setObject('secondNumber', 21); // { firstNumber: 55, secondNumber: 21 } setObject('firstNumber', 1); // { firstNumber: 1, secondNumber: 21 }
This function is NOT pure because it mutates myObj
which is in the global scope. In fact, all this function does is create side effects (bleh).
Instead, pure functions need to be deterministic, that is, they need to produce the same output for the same input regardless of how many times the function is called.
This property is also known as idempotence. This makes the function predictable, easier to reason about, and easier to test.
Here is a "purified" version of the setObject
function:
function setObjectPure(a, b) { var myObj = { a: b }; return myObj; } // all of these are separate objects setObject('firstNumber', 55); // { firstNumber: 55 } setObject('secondNumber', 21); // { secondNumber: 21 } setObject('firstNumber', 1); // { firstNumber: 1 }
Notice how a new object is created in every function call? That means setObjectPure
is side-effect free now.
Let's try to identify some pure and impure functions:
Are the following functions pure or impure?
var arr = [2, 4, 6]; function doubleValues(arr) { for(var i = 0; i < arr.length; i++){ arr[i] = arr[i] * 2; } } doubleValues(arr); arr; // [4, 8, 12] doubleValues(arr); arr; // [8, 16, 24]
The function is impure because there is a side effect: we are mutating the arr
variable.
var arr = [2, 4, 6]; function doubleValues(arr) { return arr.map(function(val){ return val * 2; }); } doubleValues(arr); // [4, 8, 12] doubleValues(arr); // [4, 8, 12] doubleValues(arr); // [4, 8, 12]
This function is pure because there is no side effect. If we wanted to double the result of double, we could combine these functions together! doubleValues(doubleValues(arr)) // [8, 16, 24]
and we still would not change the arr
variable. Pretty cool!
How about this one?
var start = {}; function addNameToObject(obj,val){ obj.name = val; return obj; } addNameToObject(start, "Elie");
The function is impure because there is a side effect: we are mutating (or changing) the start
variable. If you take a look at start
after calling addNameToObject
, you'll see that it's no longer an empty object!
var start = {}; function addNameToObject(obj,val){ var newObj = {name: val}; return Object.assign({}, obj, newObj); } addNameToObject(start, "Matt");
The function is pure because there is a not side effect and we are not mutating or changing the start
variable. Instead, we're returning a brand new object based on the structure of start
.
Here's another one:
var arr = [1, 2, 3, 4]; function addToArr(arr,val) { arr.push(val); return arr; } addToArr(arr, 5); // [1, 2, 3, 4, 5] arr; // [1, 2, 3, 4, 5]
The function is impure because there is a side effect: we are mutating (or changing) the arr
variable.
var arr = [1, 2, 3, 4]; function addToArr(arr,val) { var newArr = arr.concat(val); return newArr; } addToArr(arr, 5); // [1, 2, 3, 4, 5] arr; // [1, 2, 3, 4]
The function is pure because there is a not side effect and we are not mutating or changing the arr
variable.
You can read more about pure functions here, here, and if you are looking for a more advanced read, take a look here.
If you'd like to see an introduction to using Redux with the Chrome Dev Tools, you can watch it below.
Redux is all about reducers (hence the name). Reducers are pure functions that accept a state and an action, and return a new state. We need to make sure that we do not mutate state, so very commonly we will use conditional logic to find an action. If we do not find one, we just return the state.
It bears repeating: our reducers MUST be pure functions.
Here's an example of a reducer that updates an array of names:
function firstReducer(state=[], action){ if (action.type === 'ADD_NAME') { // return a new array with old state and new payload const newState = [...state, action.payload] return newState; } else if (action.type === 'REMOVE_NAME') { // get the index of the item to remove const idx = state.indexOf(action.payload); const newState = [...state]; // splice is used to remove 1 item at index idx newState.splice(idx, 1); return newState; } else { // by default return original state return state; } }
Be very careful when you're writing your own reducers that you aren't accidentally mutating state!
If you're using methods like pop
, shift
, unshift
, push
, or splice
on arrays, for instance, you first must create a copy of the array.
Note: Remember that composite data structures in JavaScript (Objects, Arrays) are passed by memory reference by default, whereas primitives (Numbers, Strings) are copied.
That means if you pass state
as an argument and state is an Object or Array, then it acting on it will modify it directly. This happens even if you define another variable pointing to it, for example: const x = state
; actions on x
_will modify state as well!_
Reducers are not supposed to be called directly. Instead, fire off an action which is intercepted and processed by a reducer.
In Redux, actions are simple instructions that tell the reducer(s) how to adjust state.
They come in object form and by convention contain a type
which is, by convention, a string in UPPER_SNAKE_CASE,
and (optionally) a payload that can be anything. For example:
{ type: 'ADD_NAME', payload: 'Jimmy' }
As you may have guessed, this is an action that tells the reducer to add 'Jimmy' to the list of names in state.
Many projects maintain a file called something like actionConstants.js
that just lists all of the possible actions.
It is usually preferable to use string constants instead of inline string literals when passing actions around, for example:
actionConstants.js
const ADD_NAME = 'ADD_NAME'; const REMOVE_NAME = 'REMOVE_NAME';
This may look like unnecessary overhead, but it makes debugging easier.
Action Creators are just functions that create actions.
Sometimes people use the word action
to refer to an action creator, since the actual actions themselves are the return values of the functions:
function addName(name){ return { type: ADD_NAME, payload: name } } function removeName(name){ return { type: REMOVE_NAME, payload: name } }
These functions will dispatch actions whenever your app wants to make changes to state.
In a smaller project, you can just put all the action creators and constants in actions.js
.
In Redux, the store is an object that holds the state tree for the application. Our store accepts a single reducer (called the root reducer) and has methods for getting the state, dispatching actions, and subscribing and unsubscribing.
If we have multiple reducers, we can merge them together into a root reducer with the combineReducers
function that redux
provides. For now, we will just be starting with a single reducer.
Let's create our first store! To begin, we'll need to import createStore
from redux
and create a store with a reducer.
import { createStore } from 'redux'; // bring in the createStore method let store = createStore(firstReducer); // we can now dispatch actions and get state from the store let initialState = store.getState(); console.log(initialState); // this will be an array store.dispatch(addName('Elie')); store.dispatch(addName('Matt')); store.dispatch(addName('Tim')); console.log(store.getState()); // this object will have an array with three values store.dispatch(removeName('Elie')); console.log(store.getState()); // this array will have 2 values
If you'd like to see an introduction to using Redux with vanilla JavaScript, you can watch it below
Having a single immutable state store allows us to do some really awesome stuff like time traveling, hot module reloading, and easier debugging.
With Redux, the only way we can change state is to fire off an action, which goes through the reducer to produce a new state.
Since our store is keeping track of this, we can always revert and see changes in state! You can even replay the series of actions that you (or a user) took after visiting your app.
You can get the Chrome extention here, and here is the code necessary to use the extention with a redux application (this is a sample store):
import { createStore, compose } from 'redux'; import rootReducer from './reducers'; const store = createStore(rootReducer, compose( typeof window === 'object' && typeof window.devToolsExtension !== 'undefined' ? window.devToolsExtension() : (f) => f )); export default store;
Once this is installed, you can open up the Chrome dev tools and check out the Redux tab. Here is where the "time travelling takes place" via the following:
commit
- take any changes you have made to the redux state and set it to be the new initial state. You can do this as many times as you want.
revert
- go back to an original state (a previous commit)
reset
- undo all commits and revert to the original state
You can read more about the dev tools here.
If you'd like to see a small todo app with Redux and jQuery you can watch it below.
Great Redux Tutorial Videos from the creator of Redux, Dan Abramov
What is functional programming?
Complete the Redux Exercise
When you're ready, move on to React and Redux