Home > Designing, Others > Leveling Up with React: Redux

Leveling Up with React: Redux

This tutorial is the final of a three-part series by Brad Westfall. We’ll learn how to manage state across an entire application efficiently and in a way that can scale without dangerous complexity. We’ve come so far in our React journey, it’s worth making it across the finish line here and getting the full bang-for-our-buck out of this development approach.

Article Series

Part 1: React Router
Part 2: Container Components
Part 3: Redux (You are here!)

Redux is a tool for managing both data-state and UI-state in JavaScript applications. It’s ideal for Single Page Applications (SPAs) where managing state over time can be complex. It’s also framework-agnostic, so while it was written with React in mind, it can even be used with Angular or a jQuery application.

Plus, it was conceived from an experiment with “time travel” — true fact, we’ll get to that later!

As seen in our previous tutorial, React “flows” data through components. More specifically, this is called “unidirectional data flow” — data flows in one direction from parent to child. With this characteristic, it’s not obvious how two non parent-child components would communicate in React:

React doesn’t recommend direct component-to-component communication this way. Even if it did have features to support this approach, it’s considered poor practice by many because direct component-to-component communication is error prone and leads to spaghetti code — an old term for code that is hard to follow.

React does offer a suggestion, but they expect you to implement it on your own. Here’s a section from the React docs:

For communication between two components that don’t have a parent-child relationship, you can set up your own global event system. … Flux pattern is one of the possible ways to arrange this.

This is where Redux comes in handy. Redux offers a solution of storing all your application state in one place, called a “store”. Components then “dispatch” state changes to the store, not directly to other components. The components that need to be aware of state changes can “subscribe” to the store:

The store can be thought of as a “middleman” for all state changes in the application. With Redux involved, components don’t communicate directly between each other, but rather all state changes must go through the single source of truth, the store.

This is much different from other strategies where parts of the application communicate directly between each other. Sometimes, those strategies are argued to be error prone and confusing to reason about:

With Redux, it’s clear that all components get their state from the store. It’s also clear where components should send their state changes — also the store. The component initiating the change is only concerned with dispatching the change to the store and doesn’t have to worry about a list of other components that need the state change. This is how Redux makes data flow easier to reason about.

The general concept of using store(s) to coordinate application state is a pattern known as the Flux pattern. It’s a design pattern that compliments unidirectional data flow architectures like React. Redux resembles Flux, but how close are they?

Redux is “Flux-like”

Flux is a pattern, not a tool like Redux, so it’s not something you can download. Redux though, is a tool which was inspired by the Flux pattern, among other things like Elm. There are plenty of guides out there that compare Redux to Flux. Most of them will conclude that Redux is Flux or is Flux-like, depending on how strict one defines the rules of Flux. Ultimately, it doesn’t really matter. Facebook likes and supports Redux so much that they hired it’s primary developer, Dan Abramov.

This article assumes you’re not familiar with the Flux pattern at all. But if you are, you will notice some small differences, especially considering Redux’s three guiding principals:

1. Single Source of Truth

Redux uses only one store for all its application state. Since all state resides in one place, Redux calls this the single source of truth.

The data structure of the store is ultimately up to you, but it’s typically a deeply nested object for a real application.

This one-store approach of Redux is one of the primary differences between it and Flux’s multiple store approach.

2. State is Read-Only

According to Redux docs, “The only way to mutate the state is to emit an action, an object describing what happened.”

This means the application cannot modify the state directly. Instead, “actions” are dispatched to express an intent to change the state in the store.

The store object itself has a very small API with only four methods:

  • store.dispatch(action)
  • store.subscribe(listener)
  • store.getState()
  • replaceReducer(nextReducer)

So as you can see, there’s no method for setting state. Therefore, dispatching an action is the only way for the application code to express a state change:

var action = {
  type: 'ADD_USER',
  user: {name: 'Dan'}
};

// Assuming a store object has been created already
store.dispatch(action);

The dispatch() method sends an object to Redux, known as an action. The action can be described as a “payload” that carries a type and all other data that could be used to update the state — a user in this case. Keep in mind that after the type property, the design of an action object is up to you.

3. Changes are made with Pure Functions

As just described, Redux doesn’t allow the application to make direct changes to the state. Instead, the dispatched action “describes” the state change and an intent to change state. Reducers are functions that you write which handle dispatched actions and can actually change the state.

A reducer takes in current state as an argument and can only modify the state by returning new state:

// Reducer Function
var someReducer = function(state, action) {
  ...
  return state;
}

Reducers should be written as “pure” functions, a term that describes a function with the following characteristics:

  • It does not make outside network or database calls.
  • Its return value depends solely on the values of its parameters.
  • Its arguments should be considered “immutable”, meaning they should not be changed.
  • Calling a pure function with the same set of arguments will always return the same value.

These are called “pure” because they do nothing but return a value based on their parameters. They have no side effects into any other part of the system.

Our first Redux Store

To start, create a store with Redux.createStore() and pass all reducers in as arguments. Let’s look at a small example with only one reducer:

// Note that using .push() in this way isn't the
// best approach. It's just the easiest to show
// for this example. We'll explain why in the next section.

// The Reducer Function
var userReducer = function(state, action) {
  if (state === undefined) {
    state = [];
  }
  if (action.type === 'ADD_USER') {
    state.push(action.user);
  }
  return state;
}

// Create a store by passing in the reducer
var store = Redux.createStore(userReducer);

// Dispatch our first action to express an intent to change the state
var store.dispatch({
  type: 'ADD_USER',
  user: {name: 'Dan'}
});

Here’s a brief summary of what’s happening:

  1. The store is created with one reducer.
  2. The reducer establishes that the initial state of the application is an empty array. *
  3. A dispatch is made with a new user in the action itself
  4. The reducer adds the new user to the state and returns it, which updates the store.

* The reducer is actually called twice in the example — once when the store is created and then again after the dispatch.

When the store is created, Redux immediately calls the reducers and uses their return values as initial state. This first call to the reducer sends undefined for the state. The reducer code anticipates this and returns an empty array to start the initial state of the store.

Reducers are also called each time actions are dispatched. Since the returned state from a reducer will become our new state in the store, Redux always expects reducers to return state.

In the example, the second call to our reducer comes after the dispatch. Remember, a dispatched action describes an intent to change state, and often times carries the data for the new state. This time, Redux passes the current state (still an empty array) along with the action object to the reducer. The action object, now with a type property of 'ADD_USER', allows the reducer to know how to change the state.

It’s easy to think of reducers as funnels that allow state to pass through them. This is because reducers always receive and return state to update the store:

Based on the example, our store will now be an array with one user object:

store.getState();   // => [{name: 'Dan'}]

Don’t Mutate State, Copy It

While the reducer in our example technically works, it mutates state which is poor practice. Even though reducers are responsible for changing state, they should never mutate the “current state” argument directly. This is why we shouldn’t use .push(), a mutation method, on the state argument of the reducer.

Arguments passed to the reducer should be considered immutable. In other words, they shouldn’t be directly changed. Instead of a direct mutation, we can use non-mutating methods like .concat() to essentially make a copy of the array, and then we’ll change and return the copy:

var userReducer = function(state = [], action) {
  if (action.type === 'ADD_USER') {
    var newState = state.concat([action.user]);
    return newState;
  }
  return state;
}

With this update to the reducer, adding a new user results in a copy of the state argument being changed and returned. When not adding a new user, notice the original state is returned instead of creating a copy.

There’s a whole section below on Immutable Data Structures which sheds more light on these types of best practices.

You may have also noticed that the initial state now comes form a ES2015 default parameter. So far in this series we’ve avoided ES2015 to allow you to focus on the main topics. However, Redux is much nicer with ES2015. Therefore, we’ll finally start using ES2015 in this article. Don’t worry though, each time a new ES2015 feature is used, it will be pointed out and explained.

Multiple Reducers

The last example was a nice primer, but most applications will need more complex state for the entire application. Since Redux uses just one store, we’ll need to use nested objects to organize state into different sections. Let’s imagine we want our store to resemble this object:

{
  userState: { ... },
  widgetState: { ... }
}

It’s still “one store = one object” for the entire application, but it has nested objects for userState and widgetState that can contain all kinds of data. This might seem overly simplistic, but it’s actually not that far from resembling a real Redux store.

In order to create a store with nested objects, we’ll need to define each section with a reducer:

import { createStore, combineReducers } from 'redux';

// The User Reducer
const userReducer = function(state = {}, action) {
  return state;
}

// The Widget Reducer
const widgetReducer = function(state = {}, action) {
  return state;
}

// Combine Reducers
const reducers = combineReducers({
  userState: userReducer,
  widgetState: widgetReducer
});

const store = createStore(reducers);
ES2015 Alert! The four main “variables” in this example will not be changed, so we’ll define them as constants instead. We’re also using ES2015 modules and destructuring.

The use of combineReducers() allows us to describe our store in terms of different logical sections and assign reducers to each section. Now, when each reducer returns initial state, that state will go into it’s respective userState or widgetState section of the store.

Something very important to note is that now, each reducer gets passed its respective subsection of the overall state, not the whole store’s worth of state like with the one-reducer example. Then the state returned from each reducer applies to its subsection.

Which Reducer is Called After a Dispatch?

All of them. Comparing reducers to funnels is even more apparent when we consider that each time an action is dispatched, all reducers will be called and will have an opportunity to update their respective state:

I say “their” state carefully because the reducer’s “current state” argument and its returned “updated” state only affect that reducer’s section of the store. Remember, as stated in the previous section though, each reducer only gets passed its respective state, not the whole state.

Action Strategies

There are actually quite a few strategies for creating and managing actions and action types. While they are very good to know, they aren’t as critical as some of the other information in this article. To keep the article smaller, we’ve documented the basic action strategies you should be aware of in the GitHub repo that goes along with this series.

Immutable Data Structures

The shape of the state is up to you: it can be a primitive, an array, an object, or even an Immutable.js data structure. The only important part is that you should not mutate the state object, but return a new object if the state changes.” – Redux docs

That statement says a lot, and we’ve already alluded to this point in this tutorial. If we were to start discussing the ins and outs and pros and cons of what it means to be immutable vs mutable, we could go on for a whole blog article’s worth of information. So instead I’m only going to highlight some main points.

To start:

  • JavaScript’s primitive data types (Number, String, Boolean, Undefined, and Null) are already immutable.
  • Objects, arrays, and functions are mutable.

It’s been said that mutability on data structures is prone to bugs. Since our store will be made up of state objects and arrays, we will need to implement a strategy to keep the state immutable.

Let’s imagine a state object in which we need change a property. Here are three ways:

// Example One
state.foo = '123';

// Example Two
Object.assign(state, { foo: 123 });

// Example Three
var newState = Object.assign({}, state, { foo: 123 });

The first and second examples mutate the state object. The second example mutates because Object.assign() merges all its arguments into the first argument. But this reason is also why the third example doesn’t mutate the state.

The third example merges the contents of state and{foo: 123} into a whole new blank object. This is a common trick that allows us to essentially create a copy of the state and mutate the copy without affecting the original state.

The object “spread operator” is another way to keep the state immutable:

const newState = { ...state, foo: 123 };

For a very detailed explanation of what’s going on and how this is nice for Redux, see their docs on this subject.

Object.assign() and spread operators are both ES2015.

In summary, there are many ways to explicitly keep objects and arrays immutable. Many devs use libraries like seamless-immutable, Mori, or even Facebook’s own Immutable.js.

I very carefully choose which other blogs and articles this one links to. If you’re not understanding immutability, read the reference links from above. This is a very important concept for being successful with Redux.

Initial State and Time Travel

If you read the docs, you may notice a second argument for createStore() which is for “initial state”. This might seem like an alternative to reducers creating initial state. However, this initial state should only be used for “state hydration”.

Imagine a user does a refresh on your SPA and the store’s state is reset to the reducer initial states. This might not be desired.

Instead, imagine you could have been using a strategy to persist the store and then you can re-hydrate it into Redux on the refresh. This is the reason for sending initial state into createStore().

This brings up an interesting concept though. If it’s so cheap and easy to rehydrate old state, one could imagine the equivalent of state “time travel” in their app. This can be useful for debugging or even undo/redo features. Having all your state in one store makes a lot of sense for these and many reasons! This is just one reason why immutable state helps us.

In an interview, Dan Abramov was asked “Why did you develop Redux?”

I didn’t mean to create a Flux framework. When React Europe was first announced, I proposed a talk on ‘hot reloading and time travel’ but to be honest I had no idea how to implement time travel.

Redux with React

As we’ve already discussed, Redux is framework-agnostic. Understanding Redux’s core concepts first is important before you even think about how it works with React. But now we’re ready to take a Container Component from the last article and apply Redux to it.

First, here is the original component without Redux:

import React from 'react';
import axios from 'axios';
import UserList from '../views/list-user';

const UserListContainer = React.createClass({
  getInitialState: function() {
    return {
      users: []
    };
  },

  componentDidMount: function() {
    axios.get('/path/to/user-api').then(response => {
      this.setState({users: response.data});
    });
  },

  render: function() {
    return <UserList users={this.state.users} />;
  }
});

export default UserListContainer;
ES2015 Alert! This example has been slightly converted from the original. It uses ES2015 modules and arrow functions.

Sure, it does its Ajax request and updates its own local state. But if other areas in the application need to change based on the newly acquired user list, this strategy won’t suffice.

With the Redux strategy, we can dispatch an action when the Ajax request returns instead of doing this.setState(). Then this component and others can subscribe to the state change. But this actually brings us to a question of how do we setup the store.subscribe() to update the component’s state?

I suppose I could provide several examples of manually wiring up components to the Redux store. You can probably even imagine how that might look with your own approach. But ultimately, at the end of those examples I would explain that there’s a better way, and to forget the manual examples. I would then introduce the official React/Redux binding module called react-redux. So let’s just jump straight to that.

Connecting with react-redux

Just to be clear, react, redux, and react-redux are three separate modules on npm. The react-redux module allows us to “connect” React components to Redux in a more convenient way.

Here’s what it looks like:

import React from 'react';
import { conncet } from 'react-redux';
import store from '../path/to/store';
import axios from 'axios';
import UserList from '../views/list-user';

const UserListContainer = React.createClass({
  componentDidMount: function() {
    axios.get('/path/to/user-api').then(response => {
      store.dispatch({
        type: 'USER_LIST_SUCCESS',
        users: response.data
      });
    });
  },

  render: function() {
    return <UserList users={this.props.users} />;
  }
});

const mapStateToProps = function(store) {
  return {
    users: store.userState.users
  };
}

export default connect(mapStateToProps)(UserListContainer);

There are a lot of new things going on:

  1. We’ve imported the connect function from react-redux.
  2. This code might be easier to follow from the bottom-up starting with the connection. The connect() function actually takes two arguments, but we’re only showing one for mapStateToProps().

It might look weird to see the extra set of parenthesis for connect()(). This is actually two function calls. The first, to connect() returns another function. I suppose we could have assigned that function to a name and then called it, but why do that when we can just call it immediately with the second set of parenthesis? Besides, we wouldn’t need that second function name to exist for any reason after it’s called anyways. The second function though needs you to pass a React component. In this case it’s our Container Component.

I understand if you’re thinking “why make it look more complex than it has to be?”, but this is actually a common “functional programming” paradigm, so it’s good to learn it.

  1. The first argument to connect() is a function that should return an object. The object’s properties will become “props” on the component. You can see their values come from the state. Now, I hope the function name “mapStateToProps” makes more sense. Also notice that mapStateToProps() will receive an argument which is the entire Redux store. The main idea of mapStateToProps() is to isolate which parts of the overall state this component needs as its props.
  2. For reasons mentioned in #3, we no longer need getInitialState() to exist. Also notice that we refer to this.props.users instead of this.state.users since the users array is now a prop and not local component state.
  3. The Ajax return now dispatches an action instead of updating local component state. For brevity, we’re not using action creators or action type constants.

The code example makes an assumption about how the user reducer works which may not be apparent. Notice how the store has userState property. But where did that name come from?

const mapStateToProps = function(store) {
  return {
    users: store.userState.users
  };
}

That name came from when we combined our reducers:

const reducers = combineReducers({
  userState: userReducer,
  widgetState: widgetReducer
});

What about the .users property of userState? Where did that come from?

While we didn’t show an actual reducer for the example (because it would be in another file), it’s the reducer which determines the sub properties of its respective state. To ensure .users is a property of userState, the reducer for these examples might look like this:

const initialUserState = {
  users: []
}

const userReducer = function(state = initialUserState, action) {
  switch(action.type) {
  case 'USER_LIST_SUCCESS':
    return Object.assign({}, state, { users: action.users });
  }
  return state;
}

Ajax Lifecycle Dispatches

In our Ajax example, we only dispatched one action. It was called 'USER_LIST_SUCCESS' on purpose because we may want to also dispatch 'USER_LIST_REQUEST' before the Ajax starts and 'USER_LIST_FAILED' on an Ajax failure. Be sure to read the docs on Asynchronous Actions.

Dispatching from Events

In the previous article, we saw that events should be passed down from Container to Presentational Components. It turns out react-redux helps with that too in cases where an event simply needs to dispatch an action:

...

const mapDispatchToProps = function(dispatch, ownProps) {
  return {
    toggleActive: function() {
      dispatch({ ... });
    }
  }
}

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

In the Presentation Component, we can do onClick={this.props.toggleActive} just as we did before but this time we didn’t have to write the event itself.

Container Component Omission

Sometimes, a Container Component only needs to subscribe to the store and it doesn’t need any methods like componentDidMount() to kick off Ajax requests. It may only need a render() method to pass state down to the Presentational Component. In this case, we can make a Container Component this way:

import React from 'react';
import { connect } from 'react-redux';
import UserList from '../views/list-user';

const mapStateToProps = function(store) {
  return {
    users: store.userState.users
  };
}

export default connect(mapStateToProps)(UserList);

Yes folks, that’s the whole file for our new Container Component. But wait, where’s the Container Component? And why don’t we have any use of React.createClass() here?

As it turns out, the connect() creates a Container Component for us. Notice this time we’re passing in the Presentational Component directly instead of creating our own Container Component to pass in. If you really think about what Container Components do, remember they exist to allow the Presentational Component to focus on just the view and not state. They also pass state into the child view as props. And that’s exactly what connect() does — it passes state (via props) into our Presentational Component and actually returns a React component that wraps the Presentational one. In essence, that wrapper is a Container Component.

So does that mean the examples from before are actually two Container Components wrapping a Presentational one? Sure, you can think of it that way. But that’s not a problem, it’s just only necessary when our Container Component needs more React methods besides render().

Think of the two Container Components as serving different but related roles:

Hmm, maybe that’s why the React logo looks like an atom!

Provider

In order for any of this react-redux code to work, you’ll need to let your app know how to use react-redux with a component. This component wraps your entire React application. If you’re using React Router, it would look like this:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import router from './router';

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

The store being attached to Provider is what really “connects” React and Redux via react-redux. This file is an example of what you’re main entry-point might look like.

Redux with React Router

It’s not required, but there is another npm project called react-router-redux. Since routes are technically a part of UI-state, and React Router doesn’t know about Redux, this project helps link the two.

Do you see what I did there? We went full circle and we’re back to the first article!

Final Project

The final project guide for this series allows you to make a small “Users and Widgets” Single Page App:

Final Preview

As with the other articles in this series, each comes with a guide that has even more documentation on how the guide works at GitHub.

Summary

I really hope you’ve enjoyed this series as much as I have writing it. I realize there are many topics on React we didn’t cover (forms for one), but I tried to stay true to the premise that I wanted to give new users to React a sense of how to get past the basics, and what it feels like to make a Single Page Application.

While many helped, a special thanks goes to Lynn Fisher for the amazing graphics she provided for the tutorials!


Article Series

Part 1: React Router
Part 2: Container Components
Part 3: Redux (You are here!)


Leveling Up with React: Redux is a post from CSS-Tricks

Categories: Designing, Others Tags:
  1. No comments yet.
  1. No trackbacks yet.
You must be logged in to post a comment.