Understandings of Redux Middleware

Posted on: May 29, 2023

For the past week, I have been following this Redux Tutorial to learn Redux. I found Redux middleware is the most difficult concept to understand in this tutorial. I spent a few days to study this part and tidy up a note. In this blog post, I am going to share my understandings of Redux middlewares.

Middleware

Middleware takes a dispatch method as argument, and returns a new dispatch method, as below:

js
const middleware = (dispatch) => {
const newDispatch = (action) => {
// Can do anything, including side effects, e.g.:
console.log('Hello')
// Then render the received 'dispatch' method
dispatch(action)
// Optional, can return anything
return 'Hi'
}
return newDispatch
}

Why do we often wrap middleware around to pass in store? Because store might be needed in newDispatch method, e.g. we want to use getState() from store, as shown:

js
const updatedMiddleware = (store) => {
return (dispatch) => {
const newDispatch = (action) => {
console.log(store.getState()) // We want to see the current state from store
dispatch(action)
}
return newDispatch
}
}

applyMiddleware Enhancer

To use normal middleware, applyMiddleware takes a middleware as argument, returns an enhancer

js
const applyMiddleware = (middleware) => {
const middlewareEnhancer = (createStore) => {
const newCreateStore = (reducer, preloads, enhancers) => {
const store = createStore(reducer, preloads, enhancers)
// Pass in original dispatch from store to middleware, to get a newDispatch
const newDispatch = middleware(store.dispatch)
return {...store, dispatch: newDispatch}
}
return newCreateStore
}
return middlewareEnhancer
}

And thus this enhancer can be used when creating store

js
const middlewareEnhancer = applyMiddleware(middleware)
const store = createStore(rootReducer, middlewareEnhancer)

But middlewares usually wrap with an extra layer to pass in store, so applyMiddleware needs to unwrap it first, as below:

js
const updatedApplyMiddleware = (middleware) => {
const middlewareEnhancer = (createStore) => {
const newCreateStore = (reducer, preloadedState, enhancer) => {
const store = createStore(reducer, preloadedState, enhancer)
// Unwrap updatedMiddleware by passing in store, then it will take dispatch as normal middleware
const unwrappedMiddleware = middleware(store)
const newDispatch = unwrappedMiddleware(store.dispatch)
return { ...store, dispatch: newDispatch }
}
return newCreateStore
}
return middlewareEnhancer
}

Use updatedApplyMiddleware as normal applyMiddleware, and pass in updatedMiddleware

js
const middlewareEnhancer = updatedApplyMiddleware(updatedMiddleware)
const store = createStore(rootReducer, middlewareEnhancer)

Compose Middlewares

Normally we want to use more than one middlewares, but createStore can only take in one enhancer Therefore we need to a middlewares composer that takes in an array of middlewares, and retruns a single middleware

js
const middlewaresComposer = (middlewares) => {
if (middlewares.length === 0) {
return null
}
if (middlewares.length === 1) {
return middlewares[0]
}
const firstMiddleware = middlewares.shift()
// We pass in dispatch to middleware
return (dispatch) => firstMiddleware(middlewaresComposer(middlewares)(dispatch))
}

Example of middlewaresComposer works as below:

js
const composedMiddleware = middlewaresComposer([m1, m2, m3])

The variables m1, m2, m3 are middlewares It will return a middleware as: (dispatch) => m3(m2(m1(dispatch)))

Middlewares composer can then be used in applyMiddleware:

js
const myApplyMiddleware = (middlewares) => {
const middlewareEnhancer = (createStore) => {
const newCreateStore = (reducer, preloadedState, enhancer) => {
const store = createStore(reducer, preloadedState, enhancer)
// Since middlewares wrap with extra layer to provide store, we need to unwrap all
const actualMiddlewares = middlewares.map((m) => m(store))
// Pass in the array of middlewares to get a single middleware
const composedMiddleware = middlewaresComposer(actualMiddlewares)
const newDispatch = composedMiddleware(store.dispatch)
return { ...store, dispatch: newDispatch }
}
return newCreateStore
}
return middlewareEnhancer
}

When running a chain of middlewares as below:

js
const m1 = (currDispatch) => {
const modifiedDispatch = (action) => {
console.log("hello 1!")
currDispatch(action)
console.log("bye 1!")
}
return modifiedDispatch
}
const m2 = (currDispatch) => {
const modifiedDispatch = (action) => {
console.log("hello 2!")
currDispatch(action)
console.log("bye 2!")
}
return modifiedDispatch
}
const composed = (storeDispatch) => m1(m2(storeDispatch))

We will see "hello 1!" -> "hello 2!" -> "bye 2!" -> "bye 1!" in the console Because m1's dispatch is m2(storeDispatch) => m2 is called before logging out "bye 1!" Then m2 finished running and ran the rest of m1 which prints "bye 1!"