State management in Micro-Frontends

Kaveesha Dinamidu
Sysco LABS Sri Lanka
8 min readJun 6, 2023

--

Photo by Louise Viallesoubranne on Unsplash

What is State Management?

State management refers to the process of managing the data and application state of a software application. This involves organizing and updating the information that the application relies on to function properly, such as user inputs, backend data, and application settings.

Effective state management is important for ensuring that an application runs smoothly and that users have a good experience using it. There are various techniques and tools used for state management, such as local state management, global state management, and state containers.

In web applications, state management is the process of managing and updating the data and application state of a web application. This is important for creating dynamic and interactive web pages that can respond to user inputs and provide a seamless user experience.

In client-side state management, data is managed in the user’s web browser using techniques such as cookies, local storage, and session storage. This allows for fast and efficient data retrieval and manipulation, but can be limited by the storage capacity of the user’s device and potential security concerns.

In server-side state management, data is managed on the web server, allowing for more robust data storage and retrieval capabilities. This can include techniques such as session management, caching, and server-side databases.

Some popular state management libraries and frameworks for web applications include Redux, React Context API, MobX for client-side management, and Node.js, Django, and Flask for server-side management.

What are Micro-Frontends?

Front-end web development pattern in which a single application may be built from disparate builds. It is analogous to a microservices approach but for client-side single-page applications written in JavaScript.

Micro-frontends is an architectural approach where each individual front-end feature or functionality is decoupled into its own standalone application. This allows teams to work on front-ends independently, using different frameworks and libraries for development. By breaking down front-end functionality into smaller, independent applications, teams can work more efficiently and effectively to deliver high-quality front-end experiences.

By utilizing micro-frontends, a multitude of advantages can be gained, including:

  1. Independent development: Micro-frontends allow teams to develop and deploy front-end functionality independently, without having to coordinate with other teams. This reduces dependencies and enables faster development and deployment.
  2. Flexibility: Different teams can choose different frameworks and technologies for their micro-frontends. This allows teams to work with technologies that best suit their needs and expertise.
  3. Reusability: Micro-frontends can be reused across different projects or applications. This reduces development time and promotes consistency in front-end design and functionality.
  4. Maintainability: With micro-frontends, each application is focused on a specific feature or functionality. This makes it easier to maintain and update specific parts of the front-end without affecting the rest of the application.

Do we need state management in Micro-frontends?

When state management is used between micro-frontends, it can introduce some challenges that can undermine the benefits of the micro-frontend architecture. By coupling each frontend to another, it becomes harder to manage and develop each frontend independently, defeating the purpose of using micro-frontends.

In the context of a web application, state management is used to manage the shared state across different components or pages. However, when this is extended to micro-frontends, it can lead to dependencies between the micro-frontends, making it harder to maintain and update them independently.

The diagram below illustrates the problem of coupling between micro-frontends that occurs when using state management:

As shown in the diagram, the shared state becomes a point of coupling between the micro-frontends. Changes to the shared state can have a cascading effect on the different micro-frontends that depend on it, making it harder to maintain and update them independently.

Hence, increasing the usage of state management techniques results in minimizing the interdependence between different micro frontend applications.

If we really want how can we manage state?

In certain scenarios, it becomes necessary to handle state management within the micro-frontends. In such cases, the following strategies can be implemented to effectively manage the state between different micro-frontend applications.

  1. Use local storage.

Modern web applications can conveniently access local storage without the need for additional libraries, making it a viable option for establishing a centralized location to store state variables. In this approach, each of the micro-frontend applications can access the state variables from the local storage and perform the necessary operations accordingly.

By leveraging the inherent capabilities of local storage, developers can effectively manage the state between micro-frontends in a seamless manner. It is worth noting that local storage does have certain limitations, such as the maximum size of the data that can be stored, which must be taken into consideration during the design and implementation phases.

2. Implementing a Shared API Utility

In order to enhance the performance and reliability of a microfrontend-based web application, a shared API utility microfrontend can be created. This microfrontend is responsible for caching all fetch/XHR requests and their corresponding responses, effectively reducing the amount of network requests and improving the responsiveness of the application.

All of the individual microfrontends can then call into the shared API microfrontend when making a request, giving the API microfrontend the ability to control whether or not the data needs to be refetched based on its freshness. This approach helps to minimize the overhead of network requests, improving the efficiency and reducing the risk of errors or inconsistencies across the microfrontends.

By effectively managing the data flow between different microfrontends and maintaining a consistent state across the application, developers can create a more reliable and scalable web application using the microfrontend architecture. Additionally, by caching requests and responses, the shared API microfrontend can further optimize the application’s performance, resulting in a faster and smoother user experience.

3. Create exported functions

Let’s consider this strategy using Single Spa framework. In Single SPA, you can share state between different frontend applications by exporting functions from one application and importing them in another. One way to do this is to create a shared module that exports functions that other applications can use to get or set the shared state.

For example, let’s say you have two applications: App1 and App2. App1 has some shared state that App2 needs to access. You could create a shared module in App1 that exports functions for getting and setting the shared state:

// shared.js in App1
let sharedState = { someValue: 100};

export function getSharedState() {
return sharedState;
}

export function setSharedState(newState) {
sharedState = newState;
}

Then, in App2, you can import these functions and use them to access the shared state:

// app2.js in App2
import { getSharedState, setSharedState } from 'app1/shared';

// get the shared state
const sharedState = getSharedState();

// update the shared state
setSharedState({ someValue: 900 });

Note that the shared state in this example is just a simple object, but it could be any data structure or even a state management library like Redux or MobX. Also, make sure that the shared module is loaded before the other applications that depend on it.

4. Using custom-browser events

Custom browser events can be used for state management in micro frontends by dispatching events from one micro frontend to other micro frontends that are interested in the state change. To do this:

  1. Define custom events: Define custom events that represent state changes in your micro frontends.
  2. Dispatch events: When a state change occurs in a micro frontend, dispatch the corresponding custom event using window.dispatchEvent() method.
  3. Listen for events: In the other micro frontends that are interested in the state change, listen for the custom event using window.addEventListener() method.
  4. Update state: When the custom event is received, update the state of the micro frontend accordingly.

It’s important to note that using custom browser events for state management can be complex and error-prone, especially in larger and more complex applications.

5. Using libraries

There are several libraries that can be used for state management in micro frontends. Here are some of the most popular ones:

  1. Zustand
  2. redux-micro-frontend (microsoft)

Let us explore the approach of employing the Zustand library to facilitate the distribution of state across micro frontends,

To manage the state in micro frontends using Zustand across two apps, you can use a shared state management solution to synchronize the state between the two apps. Here’s an example:

App 1:

// Define your Zustand store in App 1
import create from 'zustand';

const useStore = create((set) => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
}));

// Export your store and selectors
export { useStore };

App 2:

// Define your Zustand store in App 2
import create from 'zustand';

const useStore = create((set) => ({
message: '',
updateMessage: (newMessage) => set({ message: newMessage }),
}));

// Export your store and selectors
export { useStore };

Shared state management solution:

// In a shared file, import the stores from both apps and create a combined store
import { combine } from 'zustand';
import { useStore as useStoreApp1 } from 'App1/store';
import { useStore as useStoreApp2 } from 'App2/store';

const useCombinedStore = combine(
useStoreApp1,
useStoreApp2,
(app1Store, app2Store) => ({
count: app1Store.count,
increment: app1Store.increment,
message: app2Store.message,
updateMessage: app2Store.updateMessage,
})
);

// Export the combined store
export { useCombinedStore };

With this setup, both App 1 and App 2 have their own independent Zustand stores, but the state is synchronized between the two apps using the combine function from Zustand. The combined store provides a unified view of the state from both apps, and can be used in other components to access and modify the shared state.

Conclusion

Utilizing state management mechanisms in micro frontends is generally considered an unfavorable practice as it may result in the coupling of disparate applications, consequently undermining the benefits of adopting micro frontends. However, in certain scenarios where state sharing among micro frontends is unavoidable, it is recommended to keep the state as simple as possible to mitigate the potential drawbacks. To facilitate this, various techniques can be utilized to manage the state effectively between micro frontends, depending on the specific requirements and constraints of the application.

References

--

--

Kaveesha Dinamidu
Sysco LABS Sri Lanka

Engineering Undergraduate at the Department of Computer Science and Engineering at University of Moratuwa.