Moving Business Logic to Redux Middleware
Hey there! It's been a while since my last email, but as promised, I'm here to share more about setting up custom middleware for your business logic in your Redux application.
If you need to get back up to speed, here's the last email I wrote on the subject.
Okay, so I'm going to assume you already have some Redux experience here, and that to date, you're probably putting most of your business logic in/near your action creators.
Here's an example of an action creator that is performing a side effect:
export function login() {
return async dispatch => {
dispatch({ type: authTypes.LOG_IN_USER });
try {
const userData = await auth.login();
dispatch({ type: authTypes.AUTHENTICATED, payload: token });
} catch (error) {
dispatch({ type: authTypes.LOG_IN_USER_FAILURE, error });
}
};
}
There's nothing special going on here. We dispatch an action to let our app know we are attempting to log in, then we proceed to try logging in with await auth.login()
. Whether it succeeds or fails, we dispatch an action to let the app know. Pretty standard stuff.
But what about that pesky await auth.login()
? We're reaching outside of Redux to perform an API request — a classic example of where something can go wrong. Everything else about this action, including the error handling is happening here in Redux (one of the biggest reasons we all love Redux is how much easier it makes debugging your app), and yet we are offloading this side effect to another module.
Let's refactor this by moving this side effect to Redux middleware. This way, we will keep all the logic inside Redux so we can leave ourselves nice breadcrumbs if something goes wrong.
Start by opening your store and adding middleware. YMMV depending on your setup, but it's going to look something like this:
import { applyMiddleware, createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import { api } from '../middleware/api';
export default function configureStore(initialState = {}) {
const middleware = [api];
const store = createStore(
rootReducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);
return store;
}
We create a middleware array so we can add multiple middleware for separate features, and then we finally spread each of those middleware modules into the applyMiddleware
function.
You'll notice we imported api
but we haven't created it yet. Let's do that now.
export const api = store => next => action => {
if (action.type === API_REQUEST) {
const { method, endpoint, onSuccess, onFailure } = action.meta;
makeRequest({
action,
endpoint,
method,
body: action.payload,
store,
onSuccess,
onFailure,
});
}
return next(action);
};
Let's walk through this. First, we check if the current action type is API_REQUEST
(which is what I name the initial request event in my app). Note that middleware functions are firing on every Redux action, so as long as we are importing this middleware into our store (like we did above), we'll intercept an API request action that fires.
Next, we destructure a few values from an action.meta
object that gets passed in. Here's what the action creator looks like for this:
export const apiRequest = (method, endpoint, params, onSuccess, onFailure) => ({
type: apiTypes.API_REQUEST,
payload: params,
meta: { method, endpoint, onSuccess, onFailure },
});
Okay, I'm going to let you use your imagination on what makeRequest
looks like (in the middleware example above), but it's a function I created that takes an action object, an endpoint, a CRUD method, a body, the store object, and onSuccess
/ onFailure
actions to fire. It is what performs the request and lets us know what happened.
The difference now is Redux will be aware of when our API request starts and ends, and we'll have a nice paper trail of actions in our Redux Dev Tools that we wouldn't have had just by putting this in the action creator. The other benefit is that by moving our business logic outside of the action creator, it's much easier to try different patterns or even state management systems later on if our needs change — we can port over our makeRequest
function pretty quickly.
This is a bit contrived, but I've seen apps that have business logic sprinkled in lots of places - the UI, the action creators, even the reducer! But Redux gives us a place to put these things out of the box, and in my experience it's much easier to debug things when I can easily see all the actions firing, including the ones that have side effects.
If you want to learn more about this, there are two really good talks by Nir Kaufman, who I got the idea from.




Does this make sense? Have you used this pattern before? Do you use a different pattern? I'd love to know what you think and if I left out anything!
Talk soon,
J
I'm putting on Mississippi's first-ever dev conf and I need your help! All the details can be found in this Twitter thread. I'd really appreciate a retweet! And I'm also looking for speakers, sponsors, and volunteers. Please reach out on Twitter if you want to get involved!