React

Use redux-like middleware for useReducer in React

Advertisements

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.

Possible approaches for creating the middleware for useReducer

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, ...]);
JavaScript

You 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.

Single middleware for useReducer

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];
};
JavaScript

For 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];
};
JavaScript

Since 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];
};
JavaScript

Multiple middlewares for useReducer

Let 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];
};
JavaScript

If 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?

Middlewares after state change

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];
};
JavaScript

Then 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];
};
JavaScript

But 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];
};
JavaScript

And that completes our implementation for applying middlewares using useReducer. We can now run middlewares before and after state transitions happen in React hooks.

An example of using the middlewares

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>
  );
}
JavaScript

Now, 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
JavaScript

And thus, we have added logging as a middleware to our useReducer function!

Let us know in the comments if you have any queries.

Saransh Kataria

Born in Delhi, India, Saransh Kataria is the brain behind Wisdom Geek. Currently, Saransh is a software developer at a reputed firm in Austin, and he likes playing with new technologies to explore different possibilities. He holds an engineering degree in Computer Science. He also shares his passion for sharing knowledge as the community lead at Facebook Developer Circle Delhi, NCR which is a developer community in Delhi, India.

View Comments

  • 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 😰

    • 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.

Share
Published by
Saransh Kataria

Recent Posts

Remapping keyboard keys to avoid Carpal Tunnel

I am terrible at optimizing my keyboard layout for anything. But off lately, my little…

17 hours ago

Fixing cookies are blocked for a website with shields down on Brave

I recently switched completely to the Brave browser and have set ad blocking to aggressive…

4 months ago

Generating a QR code using Node.js

I was preparing a slide deck for a hackathon and decided to put in a…

5 months ago

How to clear the global npx cache

I have been using npx a lot lately, especially whenever I want to use a…

5 months ago

Copy/Pasting output from the terminal

Manually copy-pasting the output of a terminal command with a mouse/trackpad feels tedious. It is…

6 months ago

How To Get The Hash of A File In Node.js

While working on a project, I wanted to do an integrity check of a file…

7 months ago
Advertisements