Middleware, Store Enhancers, and other fancy words!

If all you had to ever build with Redux were ToDo applications with local, synchronous state changes, you really wouldn't need much more than what we've already talked about in previous chapters. In fact, there wouldn't be much use for Redux at all. But rarely do we build things that are so simple.

If, by contrast, you're building Twitter Lite, the official mobile web app for Twitter, there's a lot more state to manage. Without a structured approach for dealing with state, you're likely to code yourself into a corner.

So far we have a clean, structured approach to storing application state, making updates to it, and getting notified when it has changed. Cool, so that means we're done, right?! Let's all go home 😄. But if that's all we did, we'd miss out on a lot of the cool benefits we can get from having a "predictable state container" with simple serializable actions, etc.

If you think about it, since we have a starting state and a bunch of action objects dispatched to store, we should be able to repeat the same steps to end up in the same state! That's the "predictable state container" part. Since the state is a simple object with a set of actions applied in order (which are also simple objects), it's relatively easy to observe what's happening in your app. You can even persist and "replay" things that happened.

But, to build anything that does that type of thing, we'd need a way to "tap into" what's happening in Redux to observe which actions are dispatched. Plus, since we know that all changes to state result in a new copy of the state, we could even console.log the whole state object after each action to know what it contained at that particular point in time.

The first reaction to this idea, from a crafty JavaScripter like yourself, might be to overwrite store.dispatch with another function that does some logging and then calls the "real" dispatch. But that feels a bit sloppy. Unsurprisingly, Redux gives us an official way to create this type of add-on functionality.

Specifically, something called "middleware" and something called "store enhancers." Most people using Redux will not need to write store enhancers, and many will probably be quite content with using middleware written by others. But regardless, it's useful to have a high-level understanding of what they are.

  1. A store enhancer, ahem... "enhances" or adds some additional capabilities to the store. It could change how reducers process data, or how dispatch works.
  2. Middleware is a function that lets us "tap into" what's happening inside Redux when we dispatch an action.
  3. As it turns out, the ability to run middleware is added to our Redux store using a store enhancer that enhances the .dispatch() function so we don't have to hack it ourselves!

There's only one enhancer that's included in the Redux library itself. It's called applyMiddleware(), and we can pass it middleware functions to inject functionality into .dispatch().

Before we show all of that, it's worth noting that we've now covered all three arguments you can pass to createStore():

  1. The root reducer
  2. Any pre-loaded state (if it exists)
  3. A store enhancer function

It's also good to be aware that a bit of flexibility is built into the createStore function to allow you to do either:

createStore(reducer, preloadedState, enhancer)

or:

createStore(reducer, enhancer)

This way, since not all apps have preloadedState, you don't have to pass an empty null argument explicitly.

Anyway, for many folks first learning Redux you'll end up writing a createStore function that looks something like this:

import { createStore, applyMiddleware } from 'redux'
import rootReducer from './reducers'
import reduxLogger from 'redux-logger'
import reduxThunk from 'redux-thunk'

const store = createStore(rootReducer, applyMiddleware(reduxLogger, reduxThunk))

As I said, most everyone using Redux will use applyMiddleware to add some middleware; very few people will feel the need to write enhancers. So I'm not going to focus on enhancers, but let's look at this middleware thing a bit more.

So as I said, applyMiddleware lets you inject functionality into dispatch. As it turns out, you do this by writing a function, that returns a function, that returns a function. No, I'm afraid I'm not joking.

An example of middleware that literally does nothing is this:

const noOpMiddleware = store => next => action => {
  return next(action)
}

If you don't have much experience with functional programming in JavaScript, or the relatively new arrow function syntax (=>) this whole thing may look nuts.

In case it's easier to follow, the equivalent, written with regular function syntax would be:

const noOpMiddleware = function(store) {
  return function(next) {
    return function(action) {
      return next(action)
    }
  }
}

For what it's worth, I remember how to write middleware by remembering the phrase "Store next action." This helps me remember how many functions to nest, and what argument gets passed to each one. This pattern looks a bit whacky, but it allows for a lot of flexibility in what and how you dispatch actions.

As a quick example, let's write one that just logs out all actions to the console:

const loggingMiddleware = store => next => action => {
  console.log('action:', action)
  const result = next(action)
  console.log('state after action:', store.getState())
  return result
}

Hopefully, now you start to see how it can be used. The variable name store is a bit deceptive: it's just an object with the getState and dispatch methods plucked off of the store.

So, why does it work this way? Believe it or not, there's a logical explanation. Only the inner-most function runs on each dispatch. But the way JavaScript works, we always have access to the variables in the parent contexts. So, the outer two functions help set things up in a way that provides the body of the inner-most function access to things it needs.

Think of it like this:

  1. When we .dispatch() something, we dispatch the action object itself. So that's the action argument in the innermost function. It's just the action, nothing else.
  2. To enable the use of more than one middleware function there has to be a way for one middleware to be "done" as pass the action to the next piece of middleware, that's what the next argument is. It passes the action to the next piece of middleware.
  3. But ultimately, there isn't much we can do with just next(action). It doesn't give us a way to look at the current state or dispatch anything later. That's why applyMiddleware gives us access to exactly that from the outer-most function! Arguably, we should name it middlewareAPI not store, but the point is it gives us access to the relevant pieces of the store API.

"Henrik, it's still nuts, but ok... I'll bite. What can we do with this?"

Let's take a look at a common task: fetching some JSON data from an API. Unlike all the actions up we've discussed so far, this isn't just a single, synchronous state change.

Without middleware, there isn't an obvious way to do this in Redux.

Sure, we could write a function that looked like this:

const fetchData = () => {
  fetch('/some-url')
    .then(res => res.json())
    .then(result => {
      // WHERE DID STORE COME FROM?!
      store.dispatch({ type: 'DATA_FETCH_SUCCESS' })
    })
    .catch(error => {
      store.dispatch({ type: 'DATA_FETCH_FAILED' })
    })
}

I suppose you could pass in store as an argument to the outermost function. But that would break the nice pattern we had going of dispatching actions with action creators from the previous chapter: store.dispatch(doClearToDos()). Ideally, the code doing that dispatch shouldn't have to care what it has to do to clear the ToDos. In programming, ideally, we want to abstract to the point where we're fully expressing the intent, no more, no less. Anything else is what we'd call a "leaky abstraction."

What I mean is, store.dispatch(doClearToDos()) is already a pretty concise expression of intent. Getting more concise would be hard. We're dispatching this thing to the store, and presumably, our doClearToDos() function encapsulates what that entails. Why does any of this matter? Well, as we said in this example, we started out with a simple ToDo app that changes local state immediately. But then later, we decide that we also want our app to save ToDos on a remote server. The intent has not changed! You're still trying to clear all ToDos. So really, there's no reason you should have to change the part in your application view code that says when the user clicks "clear" then do store.dispatch(doClearToDos()). That part still makes perfect sense.

The question then becomes, what do we have to change? We now have three possible actions. We may want to start by dispatching a CLEAR_TODOS_STARTED action which sets a loading state that is used to show the user spinner or loading message. Then, when the request succeeds or fails, we'll either dispatch a CLEAR_TODOS_FINISHED or potentially a CLEAR_TODOS_FAILED!

To do this, we need to change how dispatch works. Functional programming and middleware to the rescue! What if, we had some middleware that instead of only letting us dispatch an object, would also allow us to dispatch a function?

First, let's look at the old synchronous version as a refresher. It simply returns an object to be dispatched:

const doClearToDos = () => {
  return { type: 'CLEAR_TODOS' }
}

Again, the answer is to use a function that returns a function! Functional programming for the win!

const doClearToDos = () => {
  // see we're returning a function!
  return dispatch => {
    // Woah!!! now somehow have access to dispatch
    // and we can start something asynchronous
    // and then sometime later use dispatch
    // to report what happened.
    dispatch({ type: 'CLEAR_TODOS_STARTED' })
    fetch('/todos', { method: 'DELETE' })
      .then(() => {
        dispatch({ type: 'CLEAR_TODOS_FINISHED' })
      })
      .catch(error => {
        dispatch({ type: 'CLEAR_TODOS_FAILED' })
      })
  }
}

I'm being explicit about returning a function in the example above to draw attention to it, but in a real app I'd write it more like this, which is equivalent to the example above:

const doClearToDos = () => dispatch => {
  dispatch({ type: 'CLEAR_TODOS_STARTED' })
  fetch('/todos', { method: 'DELETE' })
    .then(() => {
      dispatch({ type: 'CLEAR_TODOS_FINISHED' })
    })
    .catch(error => {
      dispatch({ type: 'CLEAR_TODOS_FAILED' })
    })
}

So now, what will happen when we run store.dispatch(doClearToDos())? Well wow our action creator, instead of returning a ready-to-go action object, it returns a function! A function is not an action at all! When we introduced actions a few chapters ago, we said that:

  1. Actions have to be "plain" JavaScript object
  2. They have to have a type property.

A function does not meet either of those criteria. So unless we change something about how dispatch works, Redux will throw an error if we try to dispatch a function.

Middleware to the rescue!

As it turns out, we can write middleware to let us dispatch a function! Or dispatch a promise, or whatever the heck we can think of really. Because, if you'll recall, middleware lets us stick our grubby little fingers into the dispatch process and change how it works!

So, how would we actually write middleware that lets us dispatch() a function? Well, like this:

// "Store next action" remember??
const asyncMiddleware = store => next => action => {
  // remember that "action" here is just whatever the *thing*
  // was, that was passed to `dispatch()`.
  // So we can check if it was passed a function.
  // In this case, we never call "next" at all.
  // So at this point, nothing else happens unless
  // our action creator dispatches something that is
  // a real action.
  if (typeof action === 'function') {
    // Instead, we can *call* the function that was dispatched
    // and instead, pass it the raw dispatch method from the store!
    // Now, our action creator has a reference to the dispatch method
    // and can dispatch whatever else it wants at whatever point
    // it wants to. Or, not at all.
    return action(dispatch)
  }
  // if it's not a function, just continue as normal
  return next(action)
}

As a reminder, to use this middleware, we'd now simply need to pass it to applyMiddleware() when we're creating the store:

import asyncMiddleware from './file/from/above'
import { createStore, applyMiddleware } from 'redux'

const store = createStore(rootReducer, applyMiddleware(asyncMiddleware))

That's it! We can now leave our original code unchanged: store.dispatch(doClearToDos()) but it will instead return a function that will let us dispatch multiple things, and give us the option to dispatch things later when they happen.

These are not my ideas; I'm merely explaining them. This middleware already exists, and even though it doesn't come included with the Redux library itself, Dan Abramov, the creator of Redux is the one who published it. It's called redux-thunk, and it includes what I have above, and a few more goodies.

In addition to giving you access to dispatch, it also passes getState from the store, plus it can be configured to pass in additional things via the third argument. So if you have a helper for calling your API, for example, you may do something like this:

import thunk from 'redux-thunk'
import { createStore, applyMiddleware } from 'redux'
import rootReducer from './somewhere/over/the/rainbow'
import myApiWrapper from './my/api/helper'

const store = createStore(
  rootReducer,
  applyMiddleware(thunk.withExtraArgument({ api: myApiWrapper }))
)

What other things could you do with middleware? Any number of things. You could rate-limit actions. You could wrap each next(action) in a try / catch block to report client-side errors back to the server. You could write middleware that reports analytics "events" back to the server, without having to sprinkle event-tracking code throughout your app.

You could make middleware that lets you dispatch a Promise. There are several good examples in the official documentation well worth reading. I would urge you to go reference that for ideas. I for one, tend to stick to using redux-thunk for async stuff because it does just enough to make it possible to dispatch things asynchronously and keeps things simple. I'll demonstrate how I manage more complex asynchronous flows in future chapters.

Chapter recap

  1. Middleware lets us reach into the dispatch process in Redux to change how it works.
  2. You can write middleware that enables you to dispatch a function instead of a plain object.
  3. If we write action creators that clearly express intent, there's no need to update how they are called even if we decide to change how they work. Encapsulation for the win!
  4. Store enhancers are a formal mechanism for adding capabilities to Redux itself. Most people will never need to write one.
  5. To use middleware in Redux, we use the applyMiddleware() function exported by the Redux library.
  6. applyMiddleware is itself a store enhancer that lets us change how dispatch() works.
  7. There's a library called redux-thunk written by Redux's creator that allows you to dispatch functions as was shown in the example.

results matching ""

    No results matching ""