A simple (yeah, right...) guide to using Redux with React.js

Learning React.js always starts so well. There are some sensible ideas around splitting things into components and unidirectional data flows. You figure out what state and props are for, work out how to use componentDidMount and can even get a bit of re-usability going.

Then you start trying to put a more complex application together and you find yourself propagating state and callbacks through an increasingly complex chain of components. All of a sudden you follow somebody down a rabbit-hole marked “Redux” and you're thrown into an unfamiliar world of stores, reducers, actions and lots and lots of files. Worst of all, it now takes two hours to add a button. Is this really worth the hassle and complexity tax?

Why use Redux?

You don't have to. In fact, it’s probably best to avoid it unless you have a specific and clearly understood requirement for it.

The main problem Redux tries to solve is handling state in large, complicated applications. In React you are encouraged to lift state management up the component hierarchy and pass read-only representations down as props. In larger applications you can find yourself passing state down numerous levels of a very deep hierarchy.

This is tedious to implement, more than a little brittle but also very inflexible. There's no way to share data between different branches of your component hierarchy unless you pass it down from the very top. If you want anything lower down the hierarchy to initiate changes in state then you need to pass call-backs along with state. This creates a complex web of cascading state and call-backs that can be very difficult to unpick.

There are a number of different frameworks out there that seek to address this (e.g. MobX), but it's Redux that has gained the most traction. It's hard to see why given its reputation for byzantine complexity. The problem is that learning Redux tends to involve a turgid journey through elaborate patterns and badly-framed examples. It is not for the feint-hearted.

What follows is the most stripped down, bare bones “getting started” guide I could come up with. The objective is to focus on the basic types of component and identify the way in which information flows around a Redux app.

What you'll need to know first

You'll need to be comfortable with writing components in React.js, probably to the point where you have rigged up one or two simple applications. Certainly enough to be able to understand the limitations of using React.js on its own.

There are also a few patterns you'll need to be aware of. Apart from basic concepts such as immutability and functional programming, you'll need to be familiar with the idea of container components.

This involves separating the work of fetching data in a container and rendering the UI in a corresponding component. It can give rise to cleaner, simpler components as well as promoting easier re-usability of UI elements. It can also improve the structure of an application by allowing UI components to state what data they are expecting using something like proptypes.

Containers are a nice way of separating responsibilities in React apps, but they have specific responsibilities in Redux. It's where you end up doing most of the wiring between state and the components that write out the UI.

The basics

At the heart of Redux is a state store that holds all the application data in a tree structure. You can wire up this store directly to components rather than always having to send state and call-backs down a component hierarchy.

This store is not modified directly, but indirectly through a series of specialised components with distinct responsibilities:

  • A change in state is communicated through an action. This is a component that defines the data that is changed as part of the action, e.g. a new record that has been entered by a user.
  • A reducer function is used to determine the effect that an action has on the store. For example, it could add a new record to the right part of the state tree.
  • All the logic around fetching state or dispatching actions is handled by containers. These are React components that use the Redux connect() function to wire themselves up to the store.

You can think of actions as events that are sent by React components whenever there is a change in state. The reducers are the interface between these actions and the event store. The containers in turn either dispatch actions or read the store into props that can be used by React UI components.

The example application

This example is a significantly pared down re-write of the ToDo List example on the React site. All it does is allow you to enter a new items in a text box that are displayed in a list underneath it. The end result looks like the screen shot below.

Sample application screen shot

The source code used in this tutorial can be downloaded from here (ZIP file, 10KB). Given that Redux apps are always smeared across so many different files it might be easier just to open up the source code and start stepping through it with a debugger. The application is a vanilla React.js application created using create-react-app. The only packages that have been added are redux and react-redux (a mere 897 dependencies).

The actions and reducers

This application only contains one action that is called when a reminder is added. This takes the text of the new reminder and automatically increments the identifier.

// File: /actions/index.js

// There is only one action in this app - adding a new reminder.
let nextReminderId = 0
export const addReminder = text => {
  return {
    type: 'ADD_REMINDER',
    id: nextReminderId++,
    text
  }
}

As we only have one action then we only need one reducer function. The function below accepts the action and adds the data as a new reminder to the tree:

// File: /reducers/reducer.js

// This is the only reducer in the app - it handles all the state change for the reminders
const reminders = (state = [], action) => {
  switch (action.type) {
    case 'ADD_REMINDER':
      return [
        ...state,
        {
          id: action.id,
          text: action.text
        }
      ]
    default:
      return state
  }
}
 
export default reminders

All the reducers in an application are gathered into a single component using the Redux combineReducer function. The output of this function is passed to the store when the application starts and it acts as the interface between the application and the store.

// File: /reducers/index.js

import { combineReducers } from 'redux'
import reminders from './reminders'
 
// All the reducers in the application are combined here so they can be passed to the store.
// There's only one reducer in this example, but you provide a diffeent reducer for each "chunk" of state.
const appReducers = combineReducers({
  reminders
})
 
export default appReducers

The entry point

The index.js file in the application root is where Redux sets up the store. Note the call to the createStore() method – this is where the combined reducers that we created above are passed into the store.

// File: /index.js

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import appReducers from './reducers'
import ReminderListContainer from './containers/ReminderListContainer'

// Create the store by passing in all our reducers.
let store = createStore(appReducers)

// Render the application - note that the store provider is the top-level element
render(
  <Provider store={store}>
    <div>
        <ReminderListContainer />
    </div>
  </Provider>,
  document.getElementById('root')
)

Displaying the reminders

A container component is used to read state from the store and pass it to props that will be consumed by a React component. It does this by defining a mapStateToProps function that grabs the bits of state that it is interested in.

// Write the state to the props that are sent to the ReminderList 
const mapStateToProps = state => {
  return {
    reminders:state.reminders
  }
}

A container can also define the actions that should be available to any UI components as a callback. This happens in the mapDispatchToProps method that returns a callback called addReminderAction that can be used to send our action by a UI component.

// Set up actions that are available to the ReminderList
const mapDispatchToProps = dispatch => {
  return {
    addReminderAction: text => { dispatch(addReminder(text)) }
  }
}

These two functions need to be wired up to the React component that will be displaying the UI using the React connect() command. Once this is done, both the list of reminders in the state store and the callback function that adds a new reminder will be available to UI component's props.

The example code below shows how the state and dispatch mapping methods are connected up to a UI component called ReminderList:

// Wire up the state and dispatch methods with the component that displays the UI
const ReminderListContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(ReminderList)

The full component looks like this:

// File: /containers/ReminderListContainer.js

import { connect } from 'react-redux'
import { addReminder } from '../actions'
import ReminderList from '../components/ReminderList'
 
// This container handles the interface between the state store and the ReminderList component. 

// Write the state to the props that are sent to the ReminderList 
const mapStateToProps = state => {
  return {
    reminders:state.reminders
  }
}
 
// Set up actions that are available to the ReminderList
const mapDispatchToProps = dispatch => {
  return {
    addReminderAction: text => { dispatch(addReminder(text)) }
  }
}
 
// Wire up the state and dispatch methods with the component that displays the UI
const ReminderListContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(ReminderList)
 
export default ReminderListContainer

The UI component

The component that displays the list will be the most familiar part of this example. Redux is not actually used at all in UI components as the container has already mapped to the data from the state store into props before creating the component.

For the sake of simplicity, a single component is being used here to display both the list and form used to create a new item. There are two things to note about this code:

  • The list is populated from the reminders collection in props – this has been sent from the state store by the container component.
  • The handleSubmit method invokes the callback function that has been added to props by the container component.
// File: /components/ReminderList.js

import React, { Component } from 'react';
 
// This is a UI component that displays a list of reminders.
class ReminderList extends Component {
constructor(props) {
  super(props);
  this.handleSubmit = this.handleSubmit.bind(this);
}
 
  // Handles the form submission ny invoking the "addReminderAction" callback
  handleSubmit(event) {
    event.preventDefault();
    this.props.addReminderAction(event.target.textField.value);
    event.target.textField.value = '';
  }
 
  // Render the list including a form to create new items
  // NB: Many example implementations would split this into several different components
  render() {
    return (
    <div>
 
      <form onSubmit={this.handleSubmit}>
        <input type="text" id="textField" />
        <button type="submit">Add Reminder</button>
      </form>
 
      <ul>
      { 
        this.props.reminders.map(reminder => (
        <li key={reminder.id}>{reminder.text} </li> 
        ))
      }
      </ul>
    </div>
    )}}
 
export default ReminderList

If you step through the code you can see how the action flows towards the store, while the updated state flows back to the UI:

  • The action is picked up by the “reminders” reducer function and the new item is added to the state store.
  • This triggers an update in the ReminderDisplay container which maps the new state onto a set of props.
  • These updated props are fed through to the ReminderList UI component which displays the new item.

Still here? So what's the problem with Redux?

The problem with most Redux examples is the inherent contrived complexity, i.e. do we really need so many files to display as simple list? Redux only really makes sense at scale. This makes a meaningful Redux example difficult to write as you end up creating a huge amount of scaffolding for a simple use case.

That said, contrived complexity is a genuine problem here. A pattern is more likely to be successful if it is easy to understand, clearly sign-posted in code and supported by compile-time checks. Redux offers none of these things and any implementation relies on a certain amount of self-imposed discipline. This is hard to achieve on larger projects where there will inevitably be different levels of understanding between developers.