If you have used Redux before, you would be aware of the concept of middlewares. Now that useReducer has become a commonly used react hook, we might want to replicate the idea of middleware for the useReducer hook as well.
If you do not know about middlewares, middlewares are functions that run either before or after a state transition has taken place by the reducer. It enables us to opt-in for features such as logging, crash reporting, making asynchronous API requests, etc.
In this post, we will be creating a middleware for useReducer react hook. If you want to read more about the hook and reducers in general, refer to our previous post about the useReducer React hook.
We can implement the middleware functionality in one of two ways:
1. Writing an applyMiddleware function similar to redux. This function will take in the first parameter as the reducer, and we pass the middlewares as an array in the second parameter.
This would look something like this:
const useMyReducer = applyMiddleware(useReducer, [logging, thunks, ...]);
JavaScriptYou can read more about this approach as part of this GitHub issue. The final implementation can be found here.
2. We can create a custom react hook which internally implements useReducer and gives us the functionality of passing in the middlewares as a parameter.
We will be talking about the second approach in this blog post. The first approach is acceptable too. But my opinion is that if we are thinking in terms of hooks, we should move forward with respect to hooks instead of holding on to redux patterns.
Let us first define what this custom react hook that we will be building will look like. We will start with a single middleware. Later, we will move our way up to multiple middlewares by making our implementation generic.
Our middleware for useReducer will take in a reducer as a parameter, along with the initial state. It will also take a middleware as another parameter. Therefore, our hook will be of the form:
const useReducerWithMiddleware = (reducer,
initialState,
middleware,
) => {
const [state, dispatch] = useReducer(reducer, initialState);
// TODO: middleware logic
return [state, dispatch];
};
JavaScriptFor the invocation of the middleware function, calling it inside the hook after the useReducer declaration will not be adequate. We want the middleware function to be called every time dispatch is called. Therefore, we need to return a modified function instead of directly returning dispatch.
We can solve this by using higher-order functions. We will enhance the dispatch function by creating a higher-order function around it. We will then return the higher-order function from our hook.
const useReducerWithMiddleware = (reducer,
initialState,
middleware,
) => {
const [state, dispatch] = useReducer(reducer, initialState);
const dispatchUsingMiddleware = (action) => {
middleware(action);
dispatch(action);
}
return [state, dispatchUsingMiddleware];
};
JavaScriptSince we are returning the extended dispatch function from our custom hook, we ensure that the middleware is called whenever the caller calls our custom middleware for useReducer hook.
We can even add other information such as state to the middleware call.
const useReducerWithMiddleware = (reducer,
initialState,
middleware,
) => {
const [state, dispatch] = useReducer(reducer, initialState);
const dispatchUsingMiddleware = (action) => {
middleware(action, state);
dispatch(action);
}
return [state, dispatchUsingMiddleware];
};
JavaScriptLet us expand on our previous implementation of middleware for useReducer to accept multiple middleware functions as an array.
Since all the middleware functions should be invoked before invoking dispatch, we will iterate through them all. Then, we will call dispatch.
const useReducerWithMiddleware = (reducer,
initialState,
middlewares,
) => {
const [state, dispatch] = useReducer(reducer, initialState);
const dispatchUsingMiddleware = (action) => {
middlewares.map((middleware) => middleware(action, state));
dispatch(action);
}
return [state, dispatchUsingMiddleware];
};
JavaScriptIf we were doing some asynchronous middlewares, we would have to adapt this logic to use async/await. But we will keep that part out of scope for this post.
But what if we want middlewares that get executed after the state has transitioned, aka the after the dispatch call?
If you think that we will create another input array for middlewares to be executed after the dispatch, you are absolutely correct!
However, if you thought about calling these functions right after the dispatch call, like:
const useReducerWithMiddleware = (reducer,
initialState,
middlewares,
afterDispatchMiddleWares
) => {
const [state, dispatch] = useReducer(reducer, initialState);
const dispatchUsingMiddleware = (action) => {
middlewares.map((middleware) => middleware(action, state));
dispatch(action);
afterDispatchMiddleWares.map((middleware) => middleware(action, state));
}
return [state, dispatchUsingMiddleware];
};
JavaScriptThen sadly, this would not work.
Could you think of a reason why?
It is because dispatch updates the state asynchronously.
What could be done instead?
We can wait for the state to be updated and have a callback function afterwards to handle this. We can use the useEffect hook to achieve this.
const useReducerWithMiddleware = (reducer,
initialState,
middlewares,
afterDispatchMiddleWares
) => {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
afterDispatchMiddleWares.map((middleware) => middleware(action, state));
}, [afterDispatchMiddleWares]);
const dispatchUsingMiddleware = (action) => {
middlewares.map((middleware) => middleware(action, state));
dispatch(action);
}
return [state, dispatchUsingMiddleware];
};
JavaScriptBut we do not have access to the action inside useEffect anymore. So we will need to use a ref instance variable by making use of the useRef hook. We will write the value of the action to the ref variable before calling dispatch. And then its value will be available to us inside the effect.
const useReducerWithMiddleware = (reducer,
initialState,
middlewares,
afterDispatchMiddleWares
) => {
const [state, dispatch] = useReducer(reducer, initialState);
const currentRef = useRef();
useEffect(() => {
if (!currentRef.current) return;
afterDispatchMiddleWares.map((middleware) => middleware(currentRef.current, state));
}, [afterDispatchMiddleWares, state]);
const dispatchUsingMiddleware = (action) => {
middlewares.map((middleware) => middleware(action, state));
currentRef.current = action;
dispatch(action);
}
return [state, dispatchUsingMiddleware];
};
JavaScriptAnd that completes our implementation for applying middlewares using useReducer. We can now run middlewares before and after state transitions happen in React hooks.
Let us build a basic increment counter reducer which only increments the count value. For simplicity, our reducer will always increment the count irrespective of what action is passed in. We will add two middlewares to our reducer. One will log the state before the action, and the other will log the state after it.
import useReducerWithMiddleware from "./useReducerWithMiddleware";
export default function App() {
let reducer = (state) => {
return { count: state.count + 1 };
};
const logPreviousState = (action, state) => {
console.log(`count before ${action}: ${state.count}`);
};
const logFutureState = (action, state) => {
console.log(`count after ${action}: ${state.count}`);
};
const [state, dispatch] = useReducerWithMiddleware(
reducer,
{ count: 0 },
[logPreviousState],
[logFutureState]
);
return (
<div className="App">
<span>{state.count}</span>
<button onClick={() => dispatch("increment")}>Increment Count</button>
</div>
);
}
JavaScriptNow, once we run the application and click the increment button a couple of times, we will get the console output:
count before inc: 0
count after inc: 1
count before inc: 1
count after inc: 2
JavaScriptAnd thus, we have added logging as a middleware to our useReducer function!
Let us know in the comments if you have any queries.
I am terrible at optimizing my keyboard layout for anything. But off lately, my little…
I recently switched completely to the Brave browser and have set ad blocking to aggressive…
I was preparing a slide deck for a hackathon and decided to put in a…
I have been using npx a lot lately, especially whenever I want to use a…
Manually copy-pasting the output of a terminal command with a mouse/trackpad feels tedious. It is…
While working on a project, I wanted to do an integrity check of a file…
View Comments
An example that uses `useReducerWithMiddleware` would make a nice addition to this post ;)
Thank you for the suggestion. I have added one!
Thanks for sharing. There's a problem with the afterDispatchMiddlewares firing twice, if you combine the example with a useEffect that alters some state. Try inserting this in the example code:
const [someState, setSomeState] = useState(new Date())
useEffect(() => {
console.log('count was changed')
setSomeState(new Date())
}, [state.count])
If you try, you'll see that the it will print "count after increment: 1" twice.
Hi Sune, thanks for the comment. Could you please share a bit more complete code sample? I am unable to understand what you are doing with useState here, are you trying to do it without a reducer? If you could share the code snippet with the example, I can maybe figure out what is happening.
Very useful article, thank you :) !
Now I need to figure out how to type this with Typescript 😰
haha, I hear you. The famous love-hate relationship with TypeScript 😬
Great write up. Could you share how you would adapt this to handle async middleware?
Sure, you'd want to convert the middleware to return Promise objects and then depending on pre-post state change middleware, you can either invoke it as a promise, or simply use async await:
const dispatchUsingMiddleware = async (action) => {
await middleware(action, state);
dispatch(action);
}
This starts to get a bit more complicated with multiple middlewares and chaining them which is why I kept it out of the post initially. If there is something specific that you wanted to know of, happy to answer that as well.