Search code examples
javascriptreactjsreduxreact-reduxmiddleware

What is the correct way to add additional middleware through a redux store enhancer?


I am writing a store enhancer to expose my library API through the redux store.

// consumer of the library

import React from "react";
import ReactDOM from "react-dom";
import { Provider, useStore } from "react-redux";
import { createStore, applyMiddleware, compose } from "redux";
import { myEnhancer } from "myLib";

const reducer = () => ({}); 

const store = createStore(reducer, compose(
  myEnhancer,
));

function SomeComponent() {
  const { myLib } = useStore();

  return (
    <button onClick={() => myLib.doSomething()}>
      click!
    </button>);
}

function App() {
  return (
    <Provider store={store}>
      <SomeComponent />
    </Provider>);
}

ReactDOM.render(<App />, document.getElementById("root"));

This works well, but my library also contains middleware that I would like to add to the redux store. I know I could expose the middleware as well and let the consumer of the library add it to its store:

import { myEnhancer, myMiddleware } from "myLib";

const reducer = () => ({}); 

const store = createStore(reducer, compose(
  applyMiddleware(myMiddleware),
  myEnhancer,
));

But since I am already providing a store enhancer I wonder if could not just add the middleware directly through the enhancer?

Sadly I'm unsure what the correct approach to do that is. This is how I'm trying to add the middleware:

// part of the library

import { applyMiddleware } from "redux";

const myMiddleware = (store) => (next) => (action) => {
  next(action);
  if (action.type === "demo") {
    console.log("I'm a side effect!");
  }
};

export const myEnhancer = (createStore) => (reducer, preloadedState) => {

  const store = createStore(reducer, preloadedState, applyMiddleware(
    myMiddleware
  ));

  store.myLib = {
    doSomething: () => store.dispatch({ type: "demo" }),
  };

  return store;
};

And this works!... But I must be doing it wrong because it stops working when I try to combine my enhancer with other enhancer:

// consumer of the library

// ...

// some additional middleware that the user of the library 
// would like to add to the store
const logger = (store) => (next) => (action) => {
  console.group(action.type);
  console.info("dispatching", action);
  const result = next(action);
  console.log("next state", store.getState());
  console.groupEnd();
  return result;
};

const reducer = () => ({}); 

const store = createStore(reducer, compose(
  applyMiddleware(logger), 
  myEnhancer,
  window.__REDUX_DEVTOOLS_EXTENSION__ 
    ? window.__REDUX_DEVTOOLS_EXTENSION__() 
    : (noop) => noop,
));

// ...

It works if applyMiddleware is placed behind my enhancer in compose (it should always be first though). And it always fails if I add the devtool enhancer.

How can I apply a middleware through my enhancer, so that it doesn't conflict with other enhancers such as applyMiddleware?


Solution

  • Alright so I figured this out eventually after going through the redux code for createStore and applyMiddleware step by step. Took me a while to wrap my head around what is going on but applyMiddleware simply "enhances" the store.dispatch function by chaining the middleware on top of it.

    Something like this:

    store.dispatch = (action) => {
      middleware1(store)(middleware2(store)(store.dispatch))(action);
    };
    

    We can keep adding middleware to store.dispatch and we can do that in our enhancer. This is how the enhancer looks in the end:

    export const myEnhancer = (createStore) => (...args) => {
    
      // do not mess with the args, optional enhancers needs to be passed along
      const store = createStore(...args);
    
      // add my middleware on top of store.dispatch
      // it will be called before all other middleware already added to store.dispatch 
      store.dispatch = myMiddleware(store)(store.dispatch);
    
      // very important - store.dispatch needs to be enhanced before defining the functions below. 
      // or the version of store.dispatch that they will call will not contain the 
      // middleware we just added.
    
      store.myLib = {
        doSomething: () => store.dispatch({ type: "demo" }),
      };
    
      return store;
    };
    

    My guess as to why composing with the devtools failed previously is that I wasn't passing the enhancer prop to createStore.

    Now the order in which the enhancers are passed to compose doesn't mater anymore.

    const store = createStore(reducer, compose(
      applyMiddleware(logger), // adds the logger to store.dispatch
      myEnhancer, // adds the api to store, extends store.dispatch with custom middleware
      window.__REDUX_DEVTOOLS_EXTENSION__ 
        ? window.__REDUX_DEVTOOLS_EXTENSION__() 
        : (noop) => noop,
    ));