Redux is a state-management library, often used with React. In this article, I will teach you to write custom middleware to extend the capabilities of Redux and gain an in-depth understanding of how global state is managed.
What is Redux?
If you know what Redux is, feel free to skip to the next section. Redux is used to manage global state, most often in React applications. It does so by storing a global, immutable state object, which you can modify through reducers. A reducer is a function that takes a state object, an action (in other words, event) object and returns a new state object. The action object describes what exactly happened and how the state should be changed. An action object always has a
type property, and optional payload properties. To understand it better, check out this example:
What is a middleware?
Middleware in Redux is used to override the behavior of the store, most often the
redux-thunk is one such middleware. It lets you dispatch asynchronous actions. Another example is
redux-logger, which logs every action and state change to the console.
The point of writing Redux custom middlewares is to implement some advanced features in a reusable and consistent way. It is supposed to simplify your development experience and clean up your code.
Writing Redux custom middleware
The idea of developing a custom redux middleware came to me when I encountered a problem at work. I had certain API requests to be made anytime a specific action was dispatched. Moreover, another team member had the exact same problem. I decided to develop a middleware that would allow you to intercept certain actions and provide callbacks for them. In this article, we will be developing a slightly modified version of the middleware we ended up using in production.
Middleware is defined in a slightly confusing way. It is supposed to be a function that takes in the redux store and returns a function that takes in a callback
next, which returns another function that takes in the redux action and performs the middleware feature you need. Do not worry, it would all be clear in a moment. To begin with, consider this simple middleware that simply logs the type of every dispatched action:
You can notice that on line 2 we log the type of the action and pass the action to
next pipelines the action to the next middleware or to the original dispatch, if there are no more middlewares. It is crucial to call
next, otherwise, your action will not be dispatched. Order is also important: anything before
next will be executed before the action is dispatched, anything after – after the action is dispatched.
Time to implement the intercepting feature. For it, we will write another function,
createInterceptorMiddleware, which will take a list of interceptors and return the middleware:
Now, while handling the action, we filter the interceptors by type (line 3) and call every matching interceptor’s handler. On lines 9-12 the interceptors array is defined and passed to
createInterceptorMiddleware. You can notice that we also pass the
getState to the handler, very much like redux-thunk.
Support for async handlers
While our middleware is already very helpful, it will be useful to make it support Promises for handlers, very much like redux-thunk. To implement this, we just have to dispatch the action after all the promises are resolved. Furthermore, in case any handler rejects, the action will not be dispatched:
This implementation will support async handlers and will wait for them to resolve. On lines 6-7 we check the handler’s return type, and if it is not a promise, we make it one. You can also notice that we are using
map instead of
forEach on line 5, because now we have to store the handler’s return values so
Promise.all can await them. Line 10 will dispatch the action once all promises are awaited and line 11 will log the error and not dispatch the action if any promise rejects.
Support for after-dispatch handlers
Our current implementation fires the handlers before dispatching the action. To add support for handlers that fire after dispatching the action, we will make a slight change to the handlers themselves: if a handler returns a function, this function will run after dispatch. This will give us the flexibility of having all kind of combinations and complex logic:
This feature is achieved by lines 12-17. Since
Promise.all will resolve with a list of all the handler’s return values, we iterate through it and call anything that is a function. To use this feature, the handler should return a function, like on lines 23-29.
Hook it up
Lastly, you have to actually connect your middleware to the store. You do it by passing it to the
applyMiddleware function, which will return an enhancer, which needs to be passed to the
- React.useMemo and when you should use it
- 7 really good reasons not to use TypeScript