Published on

Module Federation in Production: State Management Strategies and Best Practices

Table of Contents

Micro frontends are a new architectural pattern in web development that enables applications to be broken down into smaller, more manageable pieces. While this provides greater agility in development and deployment, it also presents challenges in terms of state management. Because each micro-frontend operates independently, ensuring that state is consistent across all parts of an application can be difficult.

Challenges with State Management in Micro Frontends

One of the biggest challenges with state management in micro frontends is ensuring consistent data across all parts of an application. Since each micro app operates independently, it can be difficult to guarantee that the data matches that of other micro apps.

One might suggest having a single data store that is shared by all micro-apps, since they combine to make a single app. While this sounds reasonable, it creates a dependency between the apps, which goes against the principle of avoiding inter-app dependency in a micro-frontend system.

Visualising this dependency in code is not easy, and testing it is also difficult because data stores are often mocked during tests.

To illustrate my point further, let me give you an example:

Let's consider apps A and B that share a single Redux store. They both use a common store to hold the user's profile information, which has the following shape:

{
  "userId": "S6nRz7bZ8Cv1dfTm",
  "name": "John Doe"
}

Now, app A received a requirement from the product team to only show the first name of the user. The developer decided to remove the name property and split it into two properties: firstName and lastName. The new state looks like this.

{
  "userId": "S6nRz7bZ8Cv1dfTm",
  "firstName": "John",
  "lastName": "Doe"
}

After refactoring their code and running all tests for app A, everything seems to be in good order. However, during deployment, app B displays the name as undefined because it expects the name property to exist in the store.

This example was simple and easy to catch when all micro-apps were in a single repository and the codebase was small. However, for larger codebases, it would not be as easy to catch.

One might suggest running all tests for apps A and B to catch the bug. However, this defeats the purpose of having a micro-frontend architecture. If you have to run tests for all apps every time the code for only one of the apps has changed, you lose the benefits of modularity and increased efficiency that a micro-frontend architecture provides.

Now, returning to the initial problem of each app having its own data store, the issue that arises is that the stores may become out of sync.

For example, suppose an app is composed of two micro-apps: Home and Profile. Each app has its own data store that stores the user's profile information to show their name. In addition, the Profile app allows the user to change their name.

When the user changes their name, the Profile app sends a request to the server and refetches the user's information, successfully showing the new name. However, when the user goes back to the Home screen, they still see the old name because the Home app has not refetched the updated data from the server.

In this case, the Profile app can modify the store data in the Home app directly, or the Home app can expose a function that the Profile app can call to notify the Home app to refetch the data from the server. However, both of these approaches create a direct dependency between the Home and Settings apps, which should be avoided in a micro-frontend architecture.

That brings us to this point: if apps cannot share a common data store, nor modify another app's data, how can we ensure that the app state is always consistent?

Approach to State Management in Micro Frontends

There are two libraries that effectively solve this problem. We will discuss them below.

Redux Micro Frontend

Redux Micro Frontend is a version of Redux specifically designed for micro frontends. It provides a way to manage state across multiple micro apps, making it easier to maintain consistency across all parts of an application. This library is developed by Microsoft.

When people think of Redux, they often associate it with ReactJS, but the library itself is actually framework agnostic. It can serve as the core of a custom state management solution, as Microsoft has done by creating Redux Micro Frontend.

The general practice is to use a single store, resulting in a single state object. However, this approach means that all Micro Frontends share the same state, which violates the Micro Frontend-based architecture’s principles. Instead, each app should be a self-contained unit with its own store.

To achieve a level of isolation, some developers use combineReducer() to write a separate reducer for each Micro Frontend and then combine them into one large reducer. While this would solve some problems, it still means that a single state object is shared across all apps. Without sufficient precautions, apps could accidentally override each other's state.

Redux Micro Frontend solves this problem by having each app create its own Redux store and a Global store which is the amalgamation of the individual stores. Micro apps can access the state of other apps by using the global store. Apps can see the store of other micro apps but cannot modify them

A small caveat:

One thing I disagree with in this library is that micro apps can still access the store of other apps. I believe this two apps should not have shared access to a single store. This design decision leads to one of the problems I explained earlier.

Tanstack Query

Tanstack Query is a library for managing server state that makes fetching, caching, synchronising, and updating server state in your web applications a breeze. Its core is not tied to any specific framework, but it offers client libraries for React, Vue, Solid, and Svelte. This allows developers to use the same library across all their projects, regardless of the framework they are using.

One of the biggest advantages of Tanstack Query is that it enables all micro apps to share a common HTTP cache. This solves the issue of having to fetch the same data multiple times from different micro apps. By sharing a cache, apps can access data from the server without creating an implicit dependency between them.

Overall, Tanstack Query is an excellent choice for state management in micro frontends. It offers a consistent and scalable solution that can be used across multiple projects and frameworks.

One caveat to note about Tanstack Query is that it does not have any way to manage client state. Therefore, you will still need a different solution, such as event passing in the browser using a pub-sub mechanism, or, using a library like RxJS

Small caveat to keep in mind

Ensure that the query keys remain consistent across all apps. Consider creating a deterministic utility function that generates a unique query key based on the URL and parameters. This will ensure that the query keys are consistent across all apps.

TLDR

The challenges with state management in micro-frontends is: how to ensure that the app state is always consistent if apps cannot share a common data store, nor modify another app's data.

Micro apps should not share a common store because it creates a direct dependency between the apps, which goes against the principle of avoiding inter-app dependency in a micro-frontend system and can lead to issues. Additionally, if each app creates its own data store, it result in the stores becoming out of sync, leading to inconsistencies in data across the micro apps.

Redux Micro Frontend and Tanstack Query are two great solutions for doing state management in a micro frontend application

This is article two of a series of articles on micro frontends. You can read the first article here.

Did you like what you read?

You can follow me on Twitter or LinkedIn to get notified when I publish new content.