TL;DR: This is a tutorial about how to keep your Redux store dynamic and flexible when migrating your frontend app to Micro-Frontends architecture, using the “Inject Reducers” technique.
Micro-Frontends is a development concept which got trendier than ever in the last year. As a reference to Micro Services architecture, Micro-Frontends’ idea is to maintain your website as a dynamic composition of features that are owned by independent teams. Likewise in HUMAN, many new exciting products have emerged recently and our Console has expanded with multiple new functionalities. We had to find a maintainable solution that let every team deliver in an isolated pipeline, without compromising on the reactive user experience of a single web app in our Console.
One of the most popular solutions for this case is Webpack’s new plugin, called Module Federation. In a nutshell, it lets you smartly configure your distributed apps’ relations and dependencies, so you can implement the Micro-Frontends concept in just a few lines of code.
But as we all know, migrations are not all roses. When your app isn’t used to being dynamic, but instead loads all the resources as a monolith, you have to deal with a lot of challenging adaptations. The biggest challenge I dealt with was the adaptation of our Redux Store to the new Module Federation architecture. Despite being the most widely used state management tool, I felt there wasn’t enough information on my use case. So, in the rest of this article, I’d like to dive into some code examples to demonstrate how it can be successfully done.
One of the basics of Redux store creation is the `configureStore`, where you should pass your reducers (combined to a “root reducer”) to initiate the newly created store. In the simple use case, your reducers’ composition is static and widely common, i.e. state and actions structure which gets loaded initially and stays there for your whole session.
const configureStore = (initialState) => {
const store = createStore(rootReducer,initialState);
return store;
}
But what if you need to make it dynamic during the session? In our Micro-Frontends case, the logic of the app can be very different between each product. Naturally, our state management is derived from this flow. Furthermore, what if you also have a set of shared reducers that are static and common to all of the products? One can say that we can initially just load all the possible reducers. But this approach is not scalable in the case of many modules, as we wouldn’t want to overload our store and hurt the performance.
At that point, I found the “Reducers Injection” technique in Redux “Code Splitting” tutorials (as recommended by the Module Federation author). As I mentioned earlier, Redux store has only one root reducer. It’s usually created using `combineReducers`, which accepts an object where the key names will become the keys in your root state object, e.g. :
{
account: { ... },
preferences: { ... },
sidebar: { ... }
}
In order to dynamically change our state, we can re-generate the reducer on demand and replace the former one. We already know that `combineReducers` can produce a root reducer, but how can we provide it to an existing store? Redux store exposes a function named `replaceReducer`, which can switch an active store root reducer with a new one. Let’s see how I wrapped all of this for the holy `injectReducer` function:
const configureStore = (initialState) => {
const createReducer = (dynamicReducers = {}) => {
return combineReducers({
...sharedReducers,
...dynamicReducers
});
};
const store = createStore(
createReducer(),
compose(applyMiddleware(...middlewares))
);
store.dynamicReducers = {};
store.injectReducer = (key, reducers) => {
store.dynamicReducers[key] = combineReducers(reducers);
store.replaceReducer(createReducer(store.dynamicReducers));
};
return store;
}
As you can see, the basic store creation looks the same, but now it’s accompanied by new helper functions. Firstly, `createReducer` is a function that optionally gets `dynamicReducers` and combines them with the `sharedReducers`, which are always required. Secondly, and very important, the `injectReducer` function. It combines the incoming reducers and then stores them as a new nested key in the global state, with the help of `replaceReducer`. If we would call store.injectReducer('BotDefender', someReducers)
, the result will be:
{
account: { ... },
preferences: { ... },
sidebar: { ... }
botDefender: {
someReducerA: { ... }
someReducerB: { ... }
}
}
And that’s how you can keep your store live and dynamic along with the user’s selected product.
The last question you might ask yourself now is “how and when should I use this injection across my application?” That is where React Hooks comes to the rescue. With the handy hooks of react-redux, we wrote a custom hook called `useInjectReducers`:
const useInjectReducers = ({ product, reducers }) => {
const dispatch = useDispatch();
const store = useStore();
const isProductReducerExists = useSelector((state) => !!state[product]);
useEffect(() => {
if (!isProductReducerExists) {
store.injectReducer(product, reducers);
}
// eslint-disable-next-line
}, []);
};
This hook is called whenever each module mounts, and so it functions as a great controller for your store when navigating between products.
When utilizing redux advanced concepts, we can keep our frontend app adaptive and performant, even when it seems complex. So, if you are a developer that is looking for assistance and inspiration with this use case, I hope you found this tutorial helpful. For any feedback and questions, please feel free to contact me.