A Practical Guide to Redux

What is Redux?

Redux is a state management library that lets you connect directly to application state from anywhere in your app. It also allows you manipulate application state from anywhere in your app. But, to work its magic, Redux requires that your app have a single data store.

This post focuses on using Redux in a React app.

This post has three parts:

  1. Boilerplate
  2. Elements of Redux (the bulk of this post)
  3. Sample app (an adorable, err, I mean super-tough 💪, Most Wanted list app)

Clone the repo.


Check out the Demo.


If you want type out the code yourself, you can get started by initializing a new project with:

create-react-app <project-name>

When create-react-app is done setting up, cd into the folder and:

npm install --save redux react-redux axios.

Axios has nothing to do with Redux, but it's the library used for AJAX calls in the demo app.

(If you don't have create-react-app installed, first run npm install -g create-react-app.)

What problem does Redux solve?

In a React app, data is fetched in an parent component and then passed down to child components through props.

Things get complicated when there are many layers of components and data/state (and the functions that modify this state) are passed through numerous components to get from origin to destination. This path can be difficult to remember and it leaves many places for errors to be introduced.

With Redux, any component can be connected directly to state.

This isn't to say that data is no longer passed down from parent components to child components via props. Rather, this path can now be direct, no passing props down from parent to great-great-great-great-great-great-grandchild.

It's not good practice to have all components connect to application state. It is best to have parent/container components connect to state and pass state directly to children.

Terminology: Components connected to state are usually called 'container' or 'smart' components, and they usually execute logic too. Components that do not have state of their own, and receive state from container components care called 'dumb' components. Sometimes these dumb components are functional components. Both container components and functional components are implemented in the demo app. (NewUserFace.js and Toast.js are functional components.)

I. Boilerplate

Here is the file structure of the demo app:

____________  
>  indicates a folder
-- indicates a file
____________

> DEMO_APP_FOLDER
  > node_modules
  > public
  > src
    > actions
       --action_one.js
       --action_two.js
       --types.js
    > components
    > reducers
       --reducer_one.js
       --reducer_two.js
       --index.js
    --index.js

All the folders contain files, but we're only concerned with actions and reducers at the moment.

To incorporate Redux into a project, two crucial bits of boilerplate need to be added to two files; the main reducer file (line 15), and the main app file (line 16).

The index.js file on line 16 is the where the main configuration is located.

import React from 'react';  
import { render } from 'react-dom';  
import { Provider } from 'react-redux';  
import { createStore, applyMiddleware } from 'redux';  
import thunk from 'redux-thunk';  
import reducers from './reducers';

import App from './components/App';  
import 'spectre.css/dist/spectre.min.css';  
import './style.css';

const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);  
const store = createStoreWithMiddleware(reducers);

render(  
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

Lines 3 and 4 import Redux functionality, and line 5 pulls in middleware that allows Redux to perform asynchronous operations (e.g. fetching data). The middleware used in this tutorial is Thunk, created by Dan Abramov, the creator of Redux.

Line 6 pulls in the functions that manage application state. These functions are called 'reducers' – more on them in the next section.

The boilerplate occurs on lines 12, 13, and 16 - 18. Lines 12 and 13 create the application state Redux will access, and Thunk is incorporated into the data store. Lines 16 to 18 wrap the main <App /> component with Redux's Provider. This connects the Redux state to the React app.

Moving on to the reducer index.js file, the combineReducers function needs to be set up here.

import { combineReducers } from 'redux';  
import PersonReducer from './reducer_person';  
import ToastReducer from './reducer_toast';

const rootReducer = combineReducers({  
  wantedList: PersonReducer,
  toast: ToastReducer
});

export default rootReducer;  

This file imports all the reducers, and attaches each reducer to a single property of application state. The PersonReducer manages all data attached to the wantedList property. The same goes for toasts (toasts are messages/alerts that usually appear in the upper right of a UI).

II. Redux Elements

For the purposes of this post, let's say there are five elements of Redux. The first three are the major players.

(1) Actions
(2) Action Creators
(3) Reducers

The last two are the 'glue' that connects Redux to your React components:

(4) The connect function imported from the 'react-redux' library.
(5) The bindActionCreators function imported from the 'redux' library.

Actions

Actions are central to Redux, and they're quite simple. An action is a regular JavaScript object that usually has two properties, type and payload. Only type is required, and you can put as many properties on an object as you'd like. I put additional properties (anything more than payload, like id or whatever else) within the payload property, so my actions are always uniform in structure.

import { ADD_PERSON } from './types';

{
  type: ADD_PERSON
  payload: putSomeDataHere
}

The constant ADD_PERSON is defined in a types.js file, usually located in the actions folder. It would look like this:

// types.js

export const ADD_PERSON = 'add_person_to_wanted_list';  
export const ANOTHER_ACTION = 'another_action';  
export const ONE_MORE_ACTION = 'one_more_action';  
Action Creators

An action creator is a function that fires off an action. The action creator itself always fired off with Redux's dispatch function (c.f. lines 4-6). The action object is passed as an argument to the action creator.

The order is: dispatch ➡️ action creator ➡️ action.

import { ADD_PERSON } from './types';

export default function addPerson(person) {  
  return dispatch => {
    dispatch(addPersonAsync(person));
  }
}

function addPersonAsync(person){  
  return {
    type: ADD_PERSON,
    payload: person
  };
}

Technically an action creator only needs to fire off Redux's dispatch function along with an object. That's not quite what's going on here. In the code above, the main function (addPerson) dispatches another function (addPersonAync). This second function is called a thunk, and it returns the object/action.

Terminology: In the context of redux-thunk, a thunk is a second function that performs delayed logic by being asynchronously returned by a first function.

What's up with that? That's the Thunk middleware at work. This double function strategy allows us to wait for an asynchronous operation (like fetching data) to complete, and then the action is returned by the thunk.

The adjusted order, including reducers, is: dispatch ➡️ action creator ➡️ thunk ➡️ action ➡️ reducer.

In this case the double function strategy isn't necessary, but it's a general pattern to follow so if an operation is asynchronous, you're already set up to deal with it.

Here's a case in which an async operation is actually taking place. Below is the code for fetching the data for the demo app's Most Wanted List.

import { GET_WANTED_LIST } from './types';  
import axios from 'axios';

export default function getWantedList() {  
  return dispatch => {
    axios.get('../wanted_list.json')
      .then(res => {
        const people = res.data.map(person => {
          person.note = 'none';
          return person;
        });
        dispatch(getUsersAsync(people));
      });
  }
}

function getUsersAsync(people){  
  return {
    type: GET_WANTED_LIST,
    payload: people
  };
}

The first dispatch is on line 5. This is where the data call happens (using Axios). Then on line 12, the action object is dispatched. It is for this action (line 12) that Redux's reducers listen.

Reducers

The ADD_PERSON action has been dispatched, so the person data attached to the action's payload property needs to be added to the array of wanted people.

Adding the new person to the state array is the job of a reducer.

Reminder: As shown in the beginning of this post, each reducer manages a single property of application state.

Reducers don't modify state, they return a new copy of state.


In other words, reducers should not use mutating methods (e.g. splice, push, pop, etc.), nor should they overwrite the values of existing properties.

Three rules regarding reducers:

  1. Reducers utilize non-mutating methods like .map() (lines 13-18 below), .filter() (line 21 below), Object.assign() (not shown), and array destructuring (line 10 below) to return a new, updated version of state.

  2. Logic should reside exclusively action creators or, better yet, utility functions used by action creators.

  3. Be sure to set the default value of state as an empty array, or an empty object (line 3 below).

Here's the person reducer (reducer_wanted_list.js) used in the demo app.

import { GET_WANTED_LIST, ADD_PERSON, UPDATE_PERSON, DELETE_PERSON } from '../actions/types';

export default function(state=[], action) {

  switch (action.type) {
    case GET_WANTED_LIST:
      return action.payload;

    case ADD_PERSON:
      return [action.payload, ...state];

    case UPDATE_PERSON:
      return state.map(person => {
        if(person.name === action.payload.name) {
          return action.payload;
        }
        return person;
      });

    case DELETE_PERSON:
      return state.filter(person => person.name !== action.payload.name);

    default:
      return state;
  }
}

Expecting something fancy? Sorry to disappoint, but a reducer is just a switch statement with a touch of data manipulation.

A reducer's switch statement reads an action's type, and then executes some non-mutating data processing. Each case in the switch statement then returns a new copy of state.

For performance purposes, neither React nor Redux does deep checking for data changes. That's why it is important to return NEW state rather than mutating previous state. A new copy of state is the signal for React to check and see if any bits of UI need to be re-rendered.

The Glue

The three essential elements have now been covered:

  • Actions.
  • Actions Creators dispatch actions.
  • Reducers listen for actions, and modify application state based on the data carried by actions.

But two pressing questions remain:

  1. How does Redux give components direct access to application state.
  2. How can we use action creators within a React app? How are they implemented?

Both questions are answered by the 'glue' functions provided by the 'redux' and 'react-redux' packages. For question #1 above, we need only the connect function. For question #2, we need both the connect, and bindActionCreators functions.

If you haven't cloned the repo yet, what's stopping you!?


connect

Question #1 is the more straight-forward of the two, so let's start there.

To connect a component to application state, you need to use the 'react-redux' connect function.

Import connect at the top of the component file.

import { connect } from 'react-redux';  

And at the bottom:

  1. Write a function with the name mapStateToProps (line 1 below).
  2. Pass mapStateToProps to the connect function (line 10).
  3. Pass your export into the connect function (line 10).

Here's what this looks like in the main <App /> component of the demo. (If you compare this to the actual code, you'll notice an important part of the component is missing from the code below. The missing code is covered next.)

function mapStateToProps(state) {  
  return {
    wantedPeople: state.wantedPeople,
    toast: state.toast
  }
}
/*
* A mystery function goes here.
*/
export default connect(mapStateToProps, null)(App);

// If you only need state, and not action 
// creators, the code above is all you need.
// The mystery function is for connecting
// action creators for use within a
// component.

Two things are happening here. First, there is the mapStateToProps function. Second, the component exports the connect function. connect glues pieces of application state to the component's props.

It is very important to note that connect takes two functions in its first set of parentheses. The first position is reserved for mapStateToProps, and can be filled by null if this function is unnecessary. The second position, which is null in this example, is reserved for the mystery function.

The mapStateToProps function has made state.wantedPeople accessible in this component under the guise of this.props.wantedPeople. The same is true for state.toast; it is now accessible via this.props.toast. The <App /> component now has direct access to application state!

<App /> can now read application state (remember, in React, props are read-only), but that's not going to cut it. There must be a way to modify state, too. This is were the second bit of Redux glue, bindActionCreators, comes in.

bindActionCreators

Action creators are the key to dispatching actions that prompt state changes (state change operations are performed by reducers, and each reducer listens for particular actions).

To glue an action creator to a component, at least three things need to be imported:

  1. the action creator (line 3 below)
  2. the Redux function bindActionCreators (line 2 below)
  3. the 'react-redux' function connect (line 1 below)

Here are the necessary imports:

import {connect} from 'react-redux';  
import {bindActionCreators} from 'redux';  
import getWantedList from '../actions/get_wanted_list'  

In the demo app, the getWantedList action performs an http GET request for the Wanted List.

Let's take another look at bottom of the <App /> file, and find out what that mystery function is all about.

function mapStateToProps(state) {  
  return {
    wantedPeople: state.wantedPeople,
    toast: state.toast
  }
}

function mapDispatchToProps(dispatch) {  
  return bindActionCreators({
    getWantedList: getWantedList
  }, dispatch);
}

export default connect(mapStateToProps, mapDispatchToProps)(App);  

With the code above in place, the getWantedList action is now accessible in the component by calling this.props.getWantedList(). The function mapDispatchToProps (line 8) returns the bindActionCreators function (line 9). mapDispatchToProps is then passed into the connect function as the second argument (line 14).

Keep in mind, the prop name can be anything. The code below makes the getWantedList action creator accessible by calling this.props.getPartyGuests().

function mapDispatchToProps(dispatch) {  
  return bindActionCreators({
    getPartyGuests: getWantedList
  }, dispatch);
}

If this is a bit confusing, don't worry:

Learn the pattern above as though it were a spell. Comprehension will follow.


In the demo app, the getWantedList action creator is fired off in the componentDidMount() lifecycle hook (this is the best place for initial data retrieval).

componentDidMount() {  
  this.props.getWantedList();
}

III. Demo App

Let's run through the cycle of

  1. Action Creator
  2. Dispatched Action
  3. Reducer operation
  4. State change and UI re-render

by following a two actions - ADD_PERSON and NEW_TOAST.

ADD_PERSON

Go to the demo app, and you'll see an 'Add' button next to the 'Most Wanted' heading. When you click that button, a modal will appear for a user to input information and create a person to add to the Most Wanted list.

When a user clicks the 'Save' button in the modal, the addPerson action creator is fired. The handlePersonCreation method in the <App /> component fires the action creator function (see line 9 below).

// a small selection from App.js
handlePersonCreation() {  
  const person = {
    name: this.state.newPersonName,
    image: `https://api.adorable.io/avatars/face/eyes${this.state.newPersonEyes}/nose${this.state.newPersonNose}/mouth${this.state.newPersonMouth}/${this.state.newPersonSkin.slice(1)}`,
    reason: this.state.newPersonReason,
    reward: this.state.newPersonReward
  };
  this.props.addPerson(person);
  this.clearFormAndCloseModal();
}

Notice that the person object is being passed to the action creator (line 9).

The addPerson action creator is available to this <App /> because it is bound to the components with connect and bindActionCreators. Check out the code yourself scrolling to the bottom of the App.js file.

// add_person.js 
import { ADD_PERSON } from './types';  
import newToast from './new_toast';

export default function addPerson(person) {  
  const message = `You've just added ${person.name} to the Most Wanted List.`;
  return dispatch => {
    dispatch(addPersonAsync(person));
    dispatch(newToast(message))
  }
}

function addPersonAsync(person){  
  return {
    type: ADD_PERSON,
    payload: person
  };

}

Something pretty cool is happening here. Two actions are being dispatched (lines 8 and 9 above)! The newToast action creator is discussed in the next section.

reducer_wanted_list.js is the reducer listening for the ADD_PERSON action.

// reducer_wanted_list.js 
import { GET_WANTED_LIST,  
         ADD_PERSON,
         UPDATE_PERSON,
         DELETE_PERSON } from '../actions/types';

export default function(state=[], action) {

  switch (action.type) {
    case GET_WANTED_LIST:
      return action.payload;

    case ADD_PERSON:
      return [action.payload, ...state];

    case UPDATE_PERSON:
      return state.map(person => {
        if(person.name === action.payload.name) {
          return action.payload;
        }
        return person;
      });

    case DELETE_PERSON:
      return state.filter(person => person.name !== action.payload.name);

    default:
      return state;
  }
}

When the ADD_PERSON action is detected, this reducer returns a newly created array by:

  • returning a new array by wrapping our code in brackets
  • destructuring the previous state array with the spread operator (...)
  • adding the new person (contained in the payload of the action object) to the beginning of the new array

Now the new person on the Wanted List is rendered first on the list. If the code were [...state, action.payload];, the new person would be rendered last on the list.

The <App /> component fired off an action creator, the action creator dispatched an action (actually two!), the reducer listened for the action and updated application state accordingly.

That's Redux at work!

NEW_TOAST

Before ending, let's take one more look at the add_person.js action creator. It dispatches two actions; in addition to dispatching ADD_PERSON, it takes a message (line 2) and passes it to the newToast action creator (line 5).

export default function addPerson(person) {  
  const message = `You've just added ${person.name} to the Most Wanted List.`;
  return dispatch => {
    dispatch(addPersonAsync(person));
    dispatch(newToast(message))
  }
}

The NEW_TOAST action creator is below.

// new_toast.js
import { NEW_TOAST } from './types';

export default function newToast(message) {  
  return dispatch => {
    dispatch(newToastAsync(message));
  }
}

function newToastAsync(message){  
  return {
    type: NEW_TOAST,
    payload: message
  };
}

The NEW_TOAST action takes the message, and delivers it to the toast reducer.

// reducer_toast.js
import { NEW_TOAST, CLEAR_TOAST } from '../actions/types';

export default function(state=null, action) {

  switch (action.type) {
    case NEW_TOAST:
      return action.payload;

    case CLEAR_TOAST:
      return '';

    default:
      return state;
  }

}

This is a very basic reducer. The NEW_TOAST listener merely takes the message and sets it to state. The toast component renders if there is a message, and it displays the message at the top of the UI. Go ahead a make create a person and see the toast. Clicking the 'x' button on the toast fires off the CLEAR_TOAST action. This clear the message from state, and since there is no message, the component is removed from the UI.

The <App /> component is able to access the toast message property of application state because it is using the connect method along with mapStateToProps.

Conclusion

Before you leave this post and demo app, it would be beneficial if you were to trace more of the Redux elements – action creators, actions, reducers, and the mapStateToProps and mapDispatchToProps functions) – throughout the demo app. If you haven't done so, go ahead and clone the repo.

Where are the action creators being fired from? What is the reducer doing?

Also throw in some console.log()s at key places in the app. Log out the data being called from the componentDidMount function. Put some logs in your reducers, both at the top and within some specific cases.

Play around with the app and make sure you know how Redux is wired up.

Prove to yourself you know how Redux works by adding some new action creators, actions, reducers, and state properties.

If you'd like to be notified when I publish new content, you can sign up for my mailing list in the navbar.