Mike's Place

An Overview of Redux Middleware

August 02, 2019

While working with large React applications, having a solid Redux architecture can keep data flow clean and easy to debug. A common point of confusion in Redux is the middleware pattern. We’ll discuss why Redux middleware is beneficial, where it fits in, and step through an implementation of it.

This guide assumes a basic understanding of React, and how the Redux action/reducer pattern works. To learn more about React, the documentation is a fantastic place to start. To learn about Redux, I recommend beginning with their basic tutorial.

What is Middleware? Why use it?

The action/reducer pattern is very clean for updating the state within an application. But what if we need to communicate with an external API? Or what if we want to log all of the actions that are dispatched? We need a way to run side effects without disrupting our action/reducer flow.

Middleware allows for side effects to be run without blocking state updates.

We can run side effects (like API requests) in response to a specific action, or in response to every action that is dispatched (like logging). There can be numerous middleware that an action runs through before ending in a reducer.

Where does Middleware fit into the Redux pattern?

First, let’s look at a redux pattern without middleware:

  1. An event occurs
  2. An action is dispatched
  3. Reducer creates a new state from the change prescribed by the action
  4. New state is passed into the React app via props

Adding middleware gives us a place to intercept actions and run side effects before or after an action occurs. After dispatching an action, it will first go through the middleware, then into the reducer.

Our new pattern with middleware now looks like:

  1. An event occurs
  2. An action is dispatched
  3. Middleware receives the action
  4. Reducer creates a new state from the change prescribed by the action
  5. New state is passed into the React app via props

What can we do in Middleware?

Anything that should take place as a result of the action, but doesn’t change state. Here’s an example that logs every action: We have access to the action itself within our middleware, so we can log it:

const loggingMiddleware = store => next => action => {
  // we will catch every action here, regardless of the type

  // log the current action
  console.log('dispatching', action)
  // continue execution of the action
  next(action)
}

We also have access to state, so we can log the current state before the action is received:

const loggingMiddleware = store => next => action => {
  console.log('dispatching', action)
  // log current state from the store
  console.log('previous state', store.getState())
  next(action)
}

After next is called with our action, we can log the new state created in response to our action:

const loggingMiddleware = store => next => action => {
  console.log('dispatching', action)
  console.log('previous state', store.getState())
  next(action)
  // log the updated state, after calling next(action)
  console.log('new state', store.getState())
}

In the example above, when we call next with the action, we are passing it down the middleware pipeline. This makes the action continue through to the following middleware, then into the reducer.

It’s important to think of middleware like a pipeline:

dispatch → middleware1 → middleware2 → ... → reducer

We dispatch one action, it runs through the middleware one at a time, ending in the reducer.

What is the difference between next and store.dispatch?

(what is called store here is sometimes referred to as middlewareApi, or just api)

Calling next continues the propagation of the current action. It’s important to not alter the action and to call it once and only once within a middleware.

Calling store.dispatch starts the new action from the beginning of the pipeline. This can be called as many times as needed from within middleware. For example, in response to receiving new data from an API call, or an error.

Use next to continue an action, use store.dispatch to send a new action.

Let’s look at an example:

middleware/api.js

const apiMiddleware = store => next => action => {
  switch (action.type) {
    // only catch a specific action
    case 'FETCH_MOVIE_DATA':
      // continue propagating the action through redux
      // this is our only call to next in this middleware
      next(action)

      // fetch data from an API that may take
      // a while to respond
      MyMovieApi.get('/movies')

        .then(res => {
          // successfully received data, dispatch a new
          // action with our data
          store.dispatch({
            type: 'SET_MOVIE_DATA',
            payload: { movies: res.data },
          })
        })
        .catch(err => {
          // received an error from the API, dispatch a
          // new action with an error
          store.dispatch({
            type: 'SET_MOVIE_DATA_ERROR',
            payload: { error: err },
          })
        })
      break

    // if we don't need to handle this action, we still
    // need to pass it along
    default:
      next(action)
  }
}

export default apiMiddleware

Using the apiMiddleware.js defined above, our application may look as follows:

A component requests data on mount, and displays data when it is received. The component only has to handle the initial request, and displaying a loading animation, error, or the data.

All of the details of fetching data, parsing data, updating state, and catching errors are handled by our Redux setup.

App.jsx

import React, { Component } from 'react'
import { connect } from 'react-redux'

import { fetchMovies } from './reducers/movies'
import LoadingAnimation from './components/LoadingAnimation'
import Error from './components/Error'

class App extends Component {
  componentDidMount() {
    // fetch movies once the component has mounted
    this.props.fetchMovies()
  }

  render() {
    // if we are loading, display a loading animation
    // instead of content
    if (this.props.isLoading) {
      return <LoadingAnimation />
    }

    // if there is an error, or we have no data to display,
    // then show an error message
    if (this.props.error || this.props.movies.length === 0) {
      return <Error />
    }

    // at this point we have everything we need
    // to display content
    return (
      <div>
        {this.props.movies.map(movie => (
          <div key={movie.id}>
            {movie.title} - {movie.director}
          </div>
        ))}
      </div>
    )
  }
}

// specify what items from state we need, and what props
// they are assigned to
const mapStateToProps = state => ({
  error: this.state.error,
  isLoading: this.state.isLoading,
  movies: this.state.movies,
})

// Specify which props the action creator is assigned to
// by passing and object for mapDispatchToProps, connect()
// will wrap them with a call to dispatch
const mapDispatchToProps = {
  fetchMovies,
}

// connect redux to our component
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(App)

reducer/movie.js

// the initial state to use
const defaultState = {
  isLoading: false,
  error: '',
  movies: [],
}

// our reducer
export default (state = defaultState, action) => {
  switch (action.type) {
    case 'FETCH_MOVIE_DATA':
      return {
        ...state,
        isLoading: true,
      }
    case 'SET_MOVIE_DATA_ERROR':
      return {
        ...state,
        isLoading: false,
        error: action.payload.error,
      }
    case 'SET_MOVIE_DATA':
      return {
        ...state,
        isLoading: false,
        error: '',
        movies: action.payload.movies,
      }
    default:
      return state
  }
}

// an Action Creator for loading data from the API
export const fetchMovies = () => ({
  type: 'FETCH_MOVIE_DATA',
})

store.js

import { applyMiddleware, combineReducers, createStore } from 'redux'
import moviesReducer from './reducer/movies'
import apiMiddleware from './middleware/api'

// combine reducers
const rootReducer = combineReducers(moviesReducer)

// create a redux store from our reducers and middleware
const store = createStore(
  rootReducer,
  applyMiddleware(apiMiddleware /* other middleware */)
)

Further Reading

For more information on understanding middleware, the following are great resources:


Mike Guida

Written by Mike Guida who enjoys prolonged outdoor adventures and building stuff with software. Follow him on Twitter