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
?
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,
));