You Aren’t Using Redux Middleware Enough

Jacob Parker
5 min readAug 22, 2016

--

Redux middleware: the thing you need to use to get redux-thunk or redux-saga to work. Also possibly one of the most underused features of Redux. Middlewares add a nice encapsulation for store behaviour that does not form part of the data, and can also make testing a lot easier.

A middleware is simply a function with the signature

storeInstance => functionToCallWithAnActionThatWillSendItToTheNextMiddleware => actionThatDispatchWasCalledWith => valueToUseAsTheReturnValueOfTheDispatchCall

Typically written as

store => next => action => result

Which you then use by using Redux’s applyMiddleware function.

const middlewares = applyMiddleware(middleware1, middleware2)
const store = createStore(reducers, initialState, middlewares)

The most basic middleware, which does literally nothing except forward the action on to the next middleware, is

const middleware = store => next => action => next(action)

And the most complicated middleware is,

const middleware = store => next => action => {
if (action.type === 'RETURN_THE_NUMBER_5') {
return 5
} else {
return next(action)
}
}

Which will make any calls to dispatch with an action whose type is RETURN_THE_NUMBER_5 firstly dispatch no actions, since we didn’t call next(action); and secondly make the dispatch call return the number 5, since we returned a value in the middleware. Or in code,

dispatch({ type: 'RETURN_THE_NUMBER_5' }) === 5
// And you won’t see that action in any of your reducers

These two things are really important to understanding middlewares. A side note is that I returned a value in both cases. This is because if there is a middleware afterwards that returns a value and if you don’t return its value, it won’t get returned from the dispatch call. Although I can almost guarantee I will not be consistent with this in the examples.

Lastly, it’s quite common to go one level higher with your middleware and include an extra closure per middleware that takes some options.

const middleware = number => store => next => action => (
action.type === 'RETURN_THE_NUMBER' ? number : next(action)
)

And you’d then apply the middleware with,

const middlewares = applyMiddleware(middleware(5))

And lastly-for-real-this-time, it is absolutely okay to have middlewares that are not completely generic. It is absolutely fine to have middlewares that only apply to your project. With that in mind,

Example 1: Encapsulating Your API

Redux-thunk allows you to provide a custom argument that could be your api. However, what if your api needs an authentication token, and you store the authentication token in your store? You could call getState in every action creator to get the token, and then pass that in to the api.

But we could use middleware to do better.

What we’ve done here is we’ve made an action creator, fetch (line 1), that will create an action that will be picked up by the middleware (line 8). When the middleware recieves this action, it will perform the fetch request and return the result (which is a promise).

To make a fetch request in an action creator, we use our fetch action creator, call it with the parameters we need, and then call dispatch with the result (line 23). It’s pretty important you call dispatch, else it won’t be picked up by the middleware, and nothing will happen.

It’s worth noting that I did not hard-code the use of window.fetch in the middleware. This makes testing really easy, since you can provide your own mock for fetch. It also makes it really easy to implement this on the server — no need for polyfills!

Example 2: localStorage and Cookies

A lot of applications need to remember user state — whether it’s an authentication token from when the user logged in or a document they were working on. We can use middleware to save the state to localStorage or cookies whenever it changes.

We can also use a trick: if you call getState before you call next(action), you’ll get the state before the action has been passed to the reducer. If you call it after, you’ll get the state after the reducer has applied the action. We can use the state before and after to only write changes to things that have actually changed.

For this example, we’ll be saving an authentication token to localStorage.

With this, you can go as generic as you need. You could pass in an array of values you want to save. You could even pass in an instance that will perform the saving, which is really useful if you want to set cookies in the browser and the server.

Example 3: File Watching

Let’s say in our application state, we have a property activeFiles, whose value is an array of files we want to watch for changes. When a file changes, we want to dispatch an action.

We’re pretty much going to use the same method as before.

What’s neat here is that it does not rely on actions: you’re free to make as many actions as you want that will change the active files, and you won’t have to change the logic in the middleware.

Technically, this and the previous example could have been done with store.subscribe — it’s a little less direct as to what happens, but it’ll work. However, the middleware approach is great for the scenario where the side-effects of the middleware do depend on the actions.

Example 4: Music Player

This example is very similar to before. However, it has an important difference: state changes can happen from the middleware and other sources.

Let’s say we have a state with the properties isPlaying and currentTime. We want to continue with the idea of a single source of truth, so the middleware has to make the music player reflect the state. If an action changes isPlaying from false to true, it will have to start playing. If an action changes currentTime in any way, it will have to seek to that time.

However, for the single source of truth to be accurate, the middleware will also have to adjust isPlaying when the music stops and change the currentTime as the music plays. This is really important to get right, because we don’t want to get the music player to seek every time the middleware updates currentTime. I generally just add an origin parameter to actions dispatched from middlewares, and then check if it is present before dispatching any actions.

And that’s it. I think this example really shows the power of Redux. We’ve done for a music player what React did for user interfaces: we have a pure state-based description of exactly what’s happening. And what’s more, so long as you get the middleware correct, you’ll never end up with bugs like where the play button shows the song is paused when it isn’t.

A side note here is that a lot of people would implement the music player as a React component. That’s definitely a legitimate way of doing things; however, there’s also a lot of cases we’re able to cover here that we would not be straight forward with the React component.

Closing Words

Redux middlewares are very versatile, and encapsulate side-effects very well. In addition, it makes it more obvious where the side-effects happen.

I mostly glossed over how middlewares are composed and the order they’re executed, and how the next function works. It’s definitely worth reading the docs on how this works.

You can also use middlewares for utility-like functions — like my own redux-enqueue, and it would be possible to implement debounce and throttle actions in the same way.

--

--