Redux Middleware Patterns

Brian Kilrain
4 min readDec 22, 2018

I get the impression redux middleware is a black hole which developers are happy to leave alone. I recently read a blog which advocated for a convoluted solution to a problem which can be very easily solved with middleware. It seems like people prefer misdirection and complexity over simple.

But at the same time, I understand the trepidation. Middleware is a powerful tool usually relegated to conversations about ‘advanced redux patterns.’ Also, since middleware are so powerful, it’s easy to shoot yourself in the foot. But if you take some time to incorporate these simple patterns into your project, you’ll find your code more maintainable and easier to reason about.

Setting Context

Firstly, let’s talk about some basic tenets we’re going to build from. I consider the following points to be best-practice:

  • Action creators ONLY return a well-formed action. No computation & no side effects.
  • Actions should represent an external event and be named as such. They should not be used as a messaging mechanism. Bad action names: SET_ACTIVE_USER, CLOSE_MODAL, . Good action names: LOGIN_BUTTON_CLICKED, MODAL_CLOSE_REQUEST. This helps you think in a reactive way.
  • Business logic should mostly live in reducers or middleware. Dumb components are more testable and generally easier to work with.
  • Components should be completely decoupled from redux. They shouldn’t care where their props come from. This makes reusability and testing a breeze.
  • Redux containers should be completely decoupled from the shape of state. This means that ‘querying’ or looking up state should be done by reusable functions, AKA selectors (check out reselect and re-reselect).
  • Following the previous point, do not keep derived state in your store… use selectors instead. So, instead of this: { numberToAddA: 1, numberToAddB: 2, result: 3 }, do this: { numberToAddA: 1, numberToAddB: 2 } and use a selector to derive the result in mapStateToProps.

Network Middleware

If your app requires data from a service, the following pattern might get you closer to the aforementioned best practices while keeping all network logic contained, maintainable, debuggable and testable.

Simple Proof of Concept

store => next => async action => {
switch(action.type) {
case SEARCH_REQUEST:
next(action)
const response = await makeSearchRequest(action.query)
store.dispatch(searchResponse(response.payload))
}
}

The snippet above does three things when a search has been initiated by a component.

  1. It passes the action onwards in the middleware chain/reducers. This is optional… perhaps you don’t require the action be run through the reducers.
  2. It makes a network request with the event data. In this case, we’re sending the query to a search endpoint.
  3. It dispatches a brand new action when the response is received. You could also use next().

What about Thunk?

Thunk is a popular library which accomplishes basically the same thing (and more) using middleware as well. I prefer the custom middleware pattern because there is less misdirection and Thunk violates the first tenet listed above (action creators should be dumb functions which return simple objects). Another bonus to network middleware is all your network logic (request/response handlers and their invocations) can live in one place so mocking, logging, debugging and maintenance is super easy.

NOTE: I’ve seen a version of this pattern where the original action is decorated with data from the response before being sent onwards. Don’t do this. Keeping actions atomic and opaque means you don’t need to use brain juice when reasoning about your code.

Fun With State Changes

While React’s setState() is asynchronous, Redux’s dispatch() is conveniently not. And since any given middleware has a reference to the store, this means we can get access to both the old and new state trees for a given action.

Simple Proof of Concept

store => next => action => {
const oldState = store.getState()
next(action)
const newState = store.getState()
// do something awesome with your newfound omnipotence
}

Because next() is synchronous, by the time newState is initialized, the action has already run through all reducers and updated state. You can use this pattern to conditionally perform other operations according to what changes in state the action introduced. Essentially, you can both peek into the future and give deeply nested reducers a bird’s eye view of the state tree.

Why would we want this?

Preventing expensive operations for noop actions

if (newState !== oldState) {
performExpensiveOperation()
}

For idempotent actions, we may want to prevent certain operations from occurring on repeated dispatches.

Validating prerequisite state

if (isTutorialMode(store.getState()) {
tutorialModeHandler(action)
} else {
next(action)
}

You may have certain flags kept in state which drastically change the way a given action should be handled. In the example above, I’m pretty sure tutorialModeHandler spawns Clippy.

NOTE: This brings up another good pattern. A middleware’s outer scope should contain only delegation code (switch and conditional statements) while the actual business logic can live in small, testable handler functions.

Web Sockets

We’ve already seen how middleware can work for you in a request/response world. There is also a pattern for interfacing with a web socket connection.

Simple Proof of Concept

store => {
// initialize connection
const socket = new WebSocket()

// set event listeners
socket.on('messageReceived', (e) => {
store.dispatch(messageReceived(e.data))
})
return next => action => {
next(action)
// emit events
switch(action.type) {
case MESSAGE_SUBMIT:
socket.send(createMessageEvent(action))
}
}
}

The example creates the socket and registers event listeners when the middleware is initialized. Then, in the innermost function, we can send messages as actions are dispatched.

If you’ve not leveraged middleware before, hopefully this has given you some ideas. If you have, please share your favorite middleware patterns in the comments. Thanks for reading!

--

--