Codementor Events

A Beginner's Guide to Redux Middleware

Published May 16, 2016Last updated Jan 18, 2017
A Beginner's Guide to Redux Middleware

Redux has become the state container of choice for React apps. The key idea that makes redux so popular is that your application logic lives in "reducers", which are JavaScript functions that take in a state and an action, and return a new state. Reducers are pure functions: they don't rely on or modify any global state, so they're easy to test, reason about, and refactor. For example, here's a redux store that keeps track of a counter:

const redux = require('redux');

const counter = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
  }
  return state;
};

const store = redux.createStore(counter);

Once you have a redux store, you can subscribe to state changes in the store and dispatch actions:

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

store.dispatch({ type: 'INCREMENT' }); // Prints "1"

Reducers are an elegant tool for managing state, but they're not the full story when it comes to building applications with redux. If you look carefully, you'll notice 2 implicit constraints on what redux reducers can do:

  • Reducers must be synchronous. They return the new state.
  • Because reducers should not modify global state, reducers should not use functions like setInterval()

Let's suppose you wanted to use redux as a state container for a stopwatch application: the application should be able to display the elapsed time on the screen and then save the elapsed time to a server using an HTTP request. Reducers are great for transforming state due to synchronous actions like button presses, but what happens when you need to throw some asynchronous behavior into the mix? That's where the idea of middleware comes in.

Introducing Middleware

Your stopwatch application needs the ability to display the amount of time elapsed. The right way to do this is for your reducer to listen for 3 actions:

  • START_TIMER, fired when the timer starts
  • TICK, fired when you should change the current amount of time elapsed
  • STOP_TIMER, fired when you're not going to receive any more TICK actions.

Here's how the reducer looks:

const stopwatch = (state = {}, action) => {
  switch (action.type) {
    case 'START_TIMER':
      return Object.assign({}, state, { startTime: action.currentTime, elapsed: 0 });
    case 'TICK':
      return Object.assign({}, state, { elapsed: action.currentTime - state.startTime });
    case 'STOP_TIMER':
      return state;
  }
  return state;
};

const store = redux.createStore(stopwatch);

The above function is just a plain old reducer: it doesn't rely on or modify global state, and it's fully synchronous. When the user clicks a button to start the timer, you can dispatch the START_TIMER event, and when they click a button to stop the timer, you dispatch STOP_TIMER.

However, there's a problem: you need to periodically dispatch TICK events to update the elapsed time. You can call setInterval() in the START_TIMER case statement, but then your reducer modifies global state and you violate redux best practices. The right place to periodically dispatch TICK events is in middleware:

const timerMiddleware = store => next => action => {
  if (action.type === 'START_TIMER') {
    action.interval = setInterval(() => store.dispatch({ type: 'TICK', currentTime: Date.now() }), 1000);
  } else if (action.type === 'STOP_TIMER') {
    clearInterval(action.interval);
  }
  next(action);
};

const stopwatch = (state = {}, action) => {
  switch (action.type) {
    case 'START_TIMER':
      return Object.assign({}, state, {
        startTime: action.currentTime,
        elapsed: 0,
        interval: action.interval
      });
    case 'TICK':
      return Object.assign({}, state, { elapsed: action.currentTime - state.startTime });
    case 'STOP_TIMER':
      return Object.assign({}, state, { interval: null });
  }
  return state;
};

const middleware = redux.applyMiddleware(timerMiddleware);
const store = redux.createStore(stopwatch, middleware);

The redux middleware syntax is a mouthful: a middleware function is a function that returns a function that returns a function. The first function takes the store as a parameter, the second takes a next function as a parameter, and the third takes the action dispatched as a parameter. The store and action parameters are the current redux store and the action dispatched, respectively. The real magic is the next() function. The next() function is what you call to say "this middleware is done executing, pass this action to the next middleware". In other words, middleware can be asynchronous.

The timerMiddleware function above is responsible for managing the setInterval() function, including clearing the interval when the STOP_TIMER action is dispatched. Redux calls the timerMiddleware function when a new action is dispatched, before the reducer. This means the middleware can transform actions as necessary, including dispatching new actions. When you run the above code in Node.js, you should see approximately the below output.

$ node
> const store = require('./test.js');
undefined
> store.subscribe(() => console.log(store.getState().elapsed));
[Function: unsubscribe]
> store.dispatch({ type: 'START_TIMER', currentTime: Date.now() });
0
undefined
> 1002
2005
3013
4015
5017
6019
7021
store.dispatch({ type: 'STOP_TIMER' })
7021
undefined
>

Great, you now have a working timer in redux! The TICK events get fired roughly once a second, so you'll get periodic state changes with the elapsed time.

Resolving Promises With Middleware

You now have a working stopwatch, but your app still needs to be able to save the time to the server. How are you going to handle HTTP requests in redux? The key is the next() function and promises:

const timerMiddleware = store => next => action => {
  if (action.type === 'START_TIMER') {
    action.interval = setInterval(() => store.dispatch({ type: 'TICK', currentTime: Date.now() }), 1000);
  } else if (action.type === 'STOP_TIMER') {
    clearInterval(action.interval);
  }
  // next() passes an action to the next middleware, or to the reducer if
  // there's no next middleware
  next(action);
};

Remember that middleware can be asynchronous. The next() function is middleware's flow control mechanism: it's how you defer control to the next middleware in the chain. You can call next() asynchronously, or even not at all. You can create a middleware that resolves promises for you. If you dispatch an action with a payload property that's a promise, the below middleware will wait for that promise to resolve or reject before calling next().

const promiseMiddleware = store => next => action => {
  // check if the `payload` property is a promise, and, if so, wait for it to resolve
  if (action.payload && typeof action.payload.then === 'function') {
    action.payload.then(
      res => { action.payload = res; next(action); },
      err => { action.error = err; next(action); });
  } else {
    // no-op if the `payload` property is not a promise
    next(action);
  }
};

You can add this middleware to your redux store using the createMiddleware() function.

// Order of execution is timerMiddleware first, promiseMiddleware second
const middleware = redux.applyMiddleware(timerMiddleware, promiseMiddleware);
const store = redux.createStore(stopwatch, middleware);

How does this help with HTTP requests in redux? HTTP clients like superagent return promise-compatible interfaces, so if you set an action's payload to a superagent request, the promiseMiddleware will wait for the request to complete before passing the action along.

const superagent = require('superagent');

store.dispatch({ type: 'SAVE_TIME', payload: superagent.post('/save', store.getState()) });

The above dispatch() call will save the elapsed time to the server. All your reducer needs to do is handle the result.

const stopwatch = (state = {}, action) => {
  switch (action.type) {
    case 'START_TIMER':
      return Object.assign({}, state, {
        startTime: action.currentTime,
        elapsed: 0,
        interval: action.interval
      });
    case 'TICK':
      return Object.assign({}, state, { elapsed: action.currentTime - state.startTime });
    case 'STOP_TIMER':
      return Object.assign({}, state, { interval: null });
    case 'SAVE_TIME':
      // If there was an error, set the error property on the state
      if (action.error) {
        return Object.assign({}, state, { error: action.error });
      }
      // Otherwise, clear all the timer state
      return Object.assign({}, state, { startTime: null, elapsed: null, error: null });
  }
  return state;
};

In the redux paradigm, your reducer should be responsible for any modifications to the state. Your middleware should not modify the state. However, your middleware should be responsible for any interactions that affect global state (like setInterval()) or any asynchronous operations (like HTTP requests).

Other Middleware Applications

While resolving promises is the most common use case for middleware, there are numerous other use cases for middleware.

  • Logging:
const loggerMiddleware = store => next => action => {
  console.log(action.type);
  next(action);
}
  • Showing a toast message when there's an error using vanillatoasts:
const toastMiddleware = store => next => action => {
  if (action.error) {
    vanillatoasts.create({ text: action.error.toString(), timeout: 5000 });
  }
  next(action);
};
const confirmationMiddleware = store => next => action => {
  if (action.shouldConfirm) {
    if (confirm('Are you sure?')) {
      next(action);
    }
  } else {
    next(action);
  }
};

Next Steps

Middleware is essential for building any non-trivial redux application. Any asynchronous behavior or global state modifications should go through middleware, so your reducers can be pure functions. If you're interested in learning more about how to use middleware to build real-world redux applications, check out this sample application, which is a medium clone written in React with Redux as the state container. This app is the basis for an upcoming video course on Thinkster about Redux, so sign up on GitHub for updates!

Discover and read more posts from Valeri Karpov
get started
post commentsBe the first to share your opinion
Sajib Arafat Siddiqui Service Holder
5 years ago

This is not beginners guide at all

Lương Đoàn
5 years ago

please give me orther way: require(’./test.js’);

eli
6 years ago

Very well written, thanks! I was wondering if there is a way to connect my custom middleware to the state. So I could check if isUser for example in order to navigate to login page if not authenticated. Is there a way to do that?

Capi Etheriel
6 years ago

You can use store.getState inside of your middlewares.

Show more replies