× {{alert.msg}} Never ask again
Online Beginner React Course: Learn React in 4 weeks
View Class

A Beginner's Guide to Redux Middleware

– {{showDate(postTime)}}
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:
    javascript const loggerMiddleware = store => next => action => { console.log(action.type); next(action); }
  • Showing a toast message when there’s an error using vanillatoasts:
    javascript const toastMiddleware = store => next => action => { if (action.error) { vanillatoasts.create({ text: action.error.toString(), timeout: 5000 }); } next(action); };
  • Waiting for the user to confirm:
    javascript 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!



Author
Valeri Karpov
Valeri Karpov
Node.js + MongoDB Open Source Contributor
Valeri Karpov is the Platform Tech Lead at Booster Fuels, where he wrangles Node.js microservices. He's a prolific contributor to the MongoDB Node.js open source ecosystem and maintains mongoose....
Hire the Author

Questions about this tutorial?  Get Live 1:1 help from React experts!
suresh atta
suresh atta
4.9
Sr Web and Java Developer by profession and your friend by nature.
Web&Java developer and loves to fix bugs. I believe in Karma and I believe in the below quote. REAL PROGRAMMER'S EYE IS A...
Hire this Expert
Bhargav
Bhargav
5.0
Experienced Full stack Developer
Bhargav is an experienced Full stack Developer who is excited about complex problems. His passion is the web and strives to help peers build...
Hire this Expert

Or Become a Codementor!

Online reactjs training course live
Online Beginner React Course:
Learn React in 4 weeks
comments powered by Disqus
Online Beginner React Course:
Learn React in 4 weeks