Redux Intro

Redux Intro

Redux is a state management tool. By now, you've likely been using React and state, setState, and props to pass data around in your application. For many simple apps, this works great. However, once you start increasing the complexity of your application or turn it into a SPA (single page app), you'll find yourself passing data down several nested component layers and increasing the ancestry-level of your state just to be able to access data everywhere in your application.

Redux takes the data component of React and turns the idea of compartmentalization on its head - instead of only having components in charge of state (whether that state is important to just that component or the app as a whole), Redux creates a global "store" (essentially state) that is accessible to the whole application.

Note: As a stepping stone, this post will focus on the bare pieces of Redux, and the examples will be presented as if you were using Redux in a vanilla JS app being run in Node.js. As such, some ES5 syntax will be used since Node.js has not adopted certain ES6 features yet (such as import and export statements). In the post about how to use Redux inside of React, we will switch back to the ES6 features that are supported and typically used in practice.

Redux: Flux Improved

Facebook pioneered the concept of Flux as a way to handle data changes in an application. Redux took the idea of Flux and added a bit to it, giving it some extra power. To better understand the Flux architecture, check out this post.

Important parts of Redux

Actions

Actions are simple objects that describe the changes that need to be made in the data store. They have at least one property, type, which represents an instruction to follow so that the store gets updated correctly. types are conventionally written in all caps and underscores (colloquially known as "Upper Snake Case", or more informally "Screaming Snake Case")

Actions can also optionally contain any sort of data included with them. Just add another property to the action object. The property name is up to you, but can be helpful if you standardize the name throughout your app, such as data or payload.

Actions are created by "Action Creators", which are just functions that return action objects. You normally won't hard code actions manually, but instead will create functions that return the action objects. Hence, the examples below are just for illustration and are not syntactically correct in terms of action creators.

// An action to increment a counter
{
    type: "INCREMENT_COUNT"
}

// An action to add a todo object to a list of todos
{
    type: "ADD_TODO",
    data: { title: "Buy milk", completed: false }
}

Action creators

Action creators are functions that, when called, return action objects. They're simple function wrappers around the objects like we showed in the above examples. If there is any data you need to include in your action, you should pass that data in as parameters to the action creator.

// Action creator to increment a count. Notice it doesn't have
// data, because it's just going to add 1 to whatever the
// current count is
function increment() {
    return {
        type: "INCREMENT"
    }
}

// Action creator to add a todo item to a list of todos
// Takes the new todo item as a parameter
function addTodo(todo) {
    return {
        type: "ADD_TODO",
        data: todo
    }
}

Dispatcher

The dispatcher sends actions to the reducer. Where the action is essentially a description of what should change and the reducer is the thing that makes the change (coming up next), the dispatcher is the vehicle that sends an action to a reducer. In Redux, the dispatcher is built in to the store and can be called with the store's dispatch() method. (We haven't covered creating the store yet, but this will make more sense once we do):

// Dispatch the increment action to the reducer
store.dispatch(increment())

// Dispatch the addTodo() action to the reducer. 
// Pass the new todo item to the action creator.
const newTodo = {
    title: "Buy Milk",
    completed: false
}
store.dispatch(addTodo(newTodo))

Reducer

The reducer's job is to take the old info from the store and update it with the new data from the action that the dispatcher gave it. It's conceptually equivalent to React's setState method.

Reducers are just functions, and they need to be "pure functions", meaning they won't alter the previous state directly, but instead will return a new state that will be used to overwrite the old state.

When you have a very simple application, you can get away with using a single reducer. Once your app scales to handle any appreciable amount of data, however, you'll want to use Redux's combineReducers function to write reducers that each handle small pieces of the store's data. For this post, we'll just do everything in a single reducer for simplicity.

The reducer function takes 2 parameters: the current version of state and the action object that the dispatcher gave it.

The reducer should look at the action object and determine what to do based on the action's type property. Typically, this decision process is handled with a JavaScript switch statement. Check the comments in the code below for further explanation.

// prevState = {} is a feature of ES6 that sets a default to
// the variable "prevState" in case it isn't defined when the 
// reducer function is called.
function reducer(prevState = {}, action) {
    switch(action.type) {
        case "INCREMENT":
            // Notice we're not using `prevState.counter++`, but instead returning
            // a NEW state object and using `prevState` only to determine what the
            // new value of the `counter` property should be. This is essential
            // to keeping the reducer as a pure function
            return {
                counter: prevState.counter + 1
            }

        case "DECREMENT":
            return {
                counter: prevState.counter - 1
            }

        case "RESET":
            return {
                counter: 0
            }
        
        // Sets the counter to a specified number. The new 
        // number was submitted as the `data` property of the
        // action object
        case "SET_TO":
            return {
                counter: action.data
            }

        // We always need a default so that state isn't modified
        // in case an action is dispatched that isn't recognized
        // by the reducer. Typically you'll just return the
        // previous state here
        default:
            return prevState
    }
}

Store

The store is the "single source of truth" in regards to the data of your application - all data that needs to be used across the entire application is kept in the store.

You use Redux's createStore function to create a new store. When doing so, you need to pass the reducer to the store so it knows how update itself whenever certain actions are dispatched. We'll see how this is done in the implementation section below.


Implementing Redux in vanilla JS

As mentioned in the beginning, this post will just cover how to use Redux in a vanilla JavaScript implementation for learning purposes. Obviously, you'll almost always use this with a JavaScript framework, typically React.

In this simple example, we'll be creating a simple counter store - stores a counter variable to keep track of the current count. We'll write action creators to increment, decrement, reset the count back to 0, and to set the count to a specified number.

Install Redux

npm install --save redux

First, we need to pull in the redux library, set up a reducer function, and initialize the store with the createStore method, passing our reducer function to the store:

Setup the store

const redux = require("redux")
const store = redux.createStore(reducer)

function reducer(prevState, action) {
    // reducer code will go here. Return a new state based 
    // on the action and prevState
}

There's a utility method on the store called subscribe() that lets us run some code every time something in the store changes. For our own benefit, lets use this in combination with another method on the store, getState(), which will return the current state object. Every time there's a change on the store, we'll log the current state of the store to the console.

...
const store = redux.createStore(reducer)
store.subscribe(() => {
    console.log(store.getState())
});

Action creators

Let's add our first action creator! Remember, it's simply a function that may or may not take a parameter, and will return an action object, which is just a plain JS object with (at least) a type property describing the kind of change we want to make.

...
function increment() {
    return { type: "INCREMENT" }
}

Easy enough, right?? We don't need to accept or pass any data because the command to "INCREMENT" should be enough information.

Whenever the dispatcher dispatches our "INCREMENT" action, it will give it to the reducer, and the reducer will figure out how to update the store accordingly.

Reducer

Let's add some code to the reducer:

function reducer(prevState = state, action) {
    switch(action.type) {
        case "INCREMENT":
            return {
                counter: prevState.counter + 1
            };
        default:
            return prevState;
    }
}

We will also figure out what our initial state will be. We want the structure of this initial state to conform to the structure of data that we return from the reducer. We will just call it state in here.

Working setup of store:

const redux = require("redux");

const state = {
    counter: 0
};

function addOne() {
    return {
        type: "INCREMENT"
    }
}

function reducer(prevState = state, action) {
    switch(action.type) {
        case "INCREMENT":
            return {
                counter: prevState.counter + 1
            };
        default:
            return prevState;
    }
}

const store = redux.createStore(reducer);
console.log(store.getState());

You can subscribe to state changes on the store. More for use in development than anything actually useful.

store.subscribe(() => {
    console.log(store.getState());
});

You need to use store.dispatch in order to run a reducer, and pass to it the action you want to start the process

const store = redux.createStore(reducer);
store.subscribe(() => {
    console.log(store.getState());
});

store.dispatch(addOne());  // { counter : 1 }
store.dispatch(addOne());  // { counter : 2 }

Review:
When you create the store, you pass the reducer function (like a tool) of what should happen when any given action is dispatched.

The store has the dispatcher built into it (store.dispatch()), which should be passed an action (an object with a "type" property at least).

With that action, the reducer will run the switch statement in order to figure out what it should do with any given action type