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:
const middleware = (dispatch) => {const newDispatch = (action) => {// Can do anything, including side effects, e.g.:console.log('Hello')// Then render the received 'dispatch' methoddispatch(action)// Optional, can return anythingreturn '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:
const updatedMiddleware = (store) => {return (dispatch) => {const newDispatch = (action) => {console.log(store.getState()) // We want to see the current state from storedispatch(action)}return newDispatch}}
applyMiddleware Enhancer
To use normal middleware, applyMiddleware takes a middleware as argument, returns an enhancer
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 newDispatchconst newDispatch = middleware(store.dispatch)return {...store, dispatch: newDispatch}}return newCreateStore}return middlewareEnhancer}
And thus this enhancer can be used when creating store
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:
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 middlewareconst 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
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
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 middlewarereturn (dispatch) => firstMiddleware(middlewaresComposer(middlewares)(dispatch))}
Example of middlewaresComposer works as below:
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:
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 allconst actualMiddlewares = middlewares.map((m) => m(store))// Pass in the array of middlewares to get a single middlewareconst composedMiddleware = middlewaresComposer(actualMiddlewares)const newDispatch = composedMiddleware(store.dispatch)return { ...store, dispatch: newDispatch }}return newCreateStore}return middlewareEnhancer}
When running a chain of middlewares as below:
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!"