Micro frontends have emerged as a popular architectural approach for scaling frontend development, especially in large applications with multiple teams working on different features. By splitting a large application into smaller, independent frontend components (or “micro frontends”), development becomes more modular, scalable, and easier to maintain. However, one of the most challenging aspects of working with micro frontends is managing state. When multiple independent micro frontends need to share or access global state, ensuring consistency and communication between these units can become complex.
In this article, we’ll explore how to approach state management in micro frontends. We’ll cover key concepts, common challenges, and effective strategies for managing local and global state within a micro frontend architecture. By the end, you’ll have a clear understanding of how to keep your micro frontends performant, maintainable, and easy to scale.
What Are Micro Frontends?
Before we dive into state management, it’s important to understand what micro frontends are. Micro frontends break up the frontend of an application into smaller, independent pieces, where each piece is owned by a different team and can be deployed independently. This approach mirrors microservices on the backend, allowing for a more decentralized and scalable development process.
In a typical micro frontend architecture:
- Each micro frontend owns its own UI, business logic, and often even its own data management.
- Micro frontends can be written in different technologies, frameworks, or versions depending on the needs of individual teams.
- They are stitched together on the client-side (often at runtime) to form a cohesive application.
While this architecture offers flexibility and scalability, managing shared state across these independent units introduces new challenges.
The Challenges of State Management in Micro Frontends
State management is always a critical factor in frontend development, but in a micro frontend architecture, it becomes even more complex. Some of the common challenges include:
Isolation vs. Shared State: Micro frontends are designed to be independent, but some state (such as user authentication or global app settings) often needs to be shared across different micro frontends. Balancing isolation with the need for shared state can be tricky.
Consistency: When different micro frontends need access to the same global state, ensuring that this state is consistent and up-to-date can be difficult, especially if different micro frontends modify that state.
Communication: Micro frontends may need to communicate with each other, particularly if changes in one micro frontend impact others. Effective communication mechanisms are essential to manage state changes.
Performance: Managing global state in large, distributed applications requires careful consideration of performance to avoid bottlenecks and ensure a responsive user experience.
Key Strategies for State Management in Micro Frontends
To effectively manage state in a micro frontend architecture, you need to strike the right balance between isolation and sharing. Below are several strategies that will help you approach state management effectively.
1. Local State First
In micro frontends, the principle of local state first is key. Each micro frontend should manage its own state independently as much as possible. This ensures that micro frontends are truly decoupled, allowing them to be developed, tested, and deployed independently.
For example, in a product page micro frontend, you might manage the local state for user interactions like sorting, filtering, or managing a shopping cart. This state is specific to that micro frontend and should not be exposed to the global application.
Using local state management libraries such as Zustand or React’s useState for small applications is sufficient. For more complex micro frontends, state management libraries like Redux or MobX can help.
Key Tip: Always start by keeping state local within your micro frontend. Only when multiple micro frontends need access to the same state should you consider lifting it to a global level.
2. Use Global State Only When Necessary
There are cases where some state needs to be shared across multiple micro frontends. Examples include:
User Authentication: A user’s login status and permissions need to be accessible by all micro frontends.
Global UI Settings: Features like theme switching (dark mode, light mode) or language preferences may need to be shared across the entire application.
Global App Data: Shared data such as user preferences, shopping cart items, or notification settings may require global access.
In these cases, a centralized global state store can be used. However, care should be taken not to overuse global state as it can introduce coupling between micro frontends, which defeats the purpose of modularity.
One approach is to create a shared global state module that all micro frontends can subscribe to. Libraries like Redux or Recoil can be used to manage this state globally. Additionally, state management libraries with support for context-based global state (like React’s Context API) are also well-suited for sharing global state across micro frontends.
Example: Sharing Global Authentication State with Redux
// Global auth state using Redux
import { createStore } from 'redux';
const initialState = {
isAuthenticated: false,
user: null,
};
const authReducer = (state = initialState, action) => {
switch (action.type) {
case 'LOGIN':
return { ...state, isAuthenticated: true, user: action.payload };
case 'LOGOUT':
return { ...state, isAuthenticated: false, user: null };
default:
return state;
}
};
const store = createStore(authReducer);
export default store;
Micro frontends can subscribe to this global authentication state to check the user’s login status.
3. Event-Driven Communication Between Micro Frontends
When micro frontends need to communicate but remain loosely coupled, using an event-driven architecture can be a powerful strategy. By emitting and listening for events, micro frontends can communicate state changes without directly sharing or modifying each other’s state.
For example, if one micro frontend handles user preferences and another micro frontend displays personalized content, the first micro frontend can emit an event when preferences are updated. The second micro frontend can listen for this event and update its UI accordingly.
Example: Event Communication Using a Global Event Bus
// EventBus.js
export const EventBus = {
events: {},
subscribe(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
},
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(callback => callback(data));
}
}
};
// Micro frontend 1: Emit an event when preferences are updated
EventBus.emit('preferencesUpdated', { theme: 'dark' });
// Micro frontend 2: Listen for the preferencesUpdated event
EventBus.subscribe('preferencesUpdated', (data) => {
console.log('Preferences updated:', data);
});
In this example, micro frontends communicate via a global event bus. This decouples the micro frontends from one another while allowing them to stay in sync.
4. URL-Based State Management
Another effective way to manage state across micro frontends is by using URL-based state management. In this approach, key pieces of state are encoded in the URL parameters or hash. Micro frontends can read and update the URL to share state globally without requiring a centralized state store.
For instance, in a dashboard micro frontend, you could include filters or view settings in the URL, such as:
https://app.example.com/dashboard?filter=recent&view=grid
When other micro frontends (e.g., a sidebar navigation) need to interact with this state, they can read the URL parameters and adjust accordingly.
Benefits of URL-Based State:
Persistence: The state is automatically persisted in the URL, so users can bookmark or share the link and come back to the same state.
SEO Benefits: For public-facing micro frontends, managing state via URLs can improve SEO by making content directly accessible via URLs.
No Global Store Overhead: By relying on the URL, you avoid the complexity of managing a global state store.
5. Managing State with Web Storage APIs (LocalStorage/SessionStorage)
If you need a lightweight solution for persisting state across sessions or browser tabs, using localStorage or sessionStorage can be a good option. This allows micro frontends to store shared state (like user preferences or cart data) in the browser’s storage.
Micro frontends can read from and write to localStorage
without needing to interact with a global state management library. This keeps the architecture simple but allows for persistent state sharing.
Example: Using LocalStorage for Shared State
// Micro frontend 1: Save preferences to localStorage
localStorage.setItem('theme', 'dark');
// Micro frontend 2: Read preferences from localStorage
const theme = localStorage.getItem('theme');
if (theme) {
// Apply the theme
}
While this approach is simple, it’s important to handle synchronization carefully, especially when state changes across different micro frontends. Polling or using the Storage event (which triggers when the storage is updated) can help synchronize state between micro frontends.
6. Use Cross-Framework State Management
In some micro frontend architectures, different teams might use different frameworks (e.g., React for one micro frontend, Vue for another). Managing global state across these heterogeneous frameworks can be challenging.
One solution is to use framework-agnostic state management tools, such as RxJS or CustomEvent API. These tools allow you to manage state and event streams independently of any frontend framework, making it easier to maintain consistency across different micro frontends.
Example: Using RxJS for State Management
import { BehaviorSubject } from 'rxjs';
// Create a global state stream using RxJS
const globalState = new BehaviorSubject({ theme: 'light' });
// Micro frontend 1: Update the theme state
globalState.next({ theme: 'dark' });
// Micro frontend 2: Subscribe to state changes
globalState.subscribe((state) => {
console.log('Theme updated to:', state.theme);
});
By using RxJS or other reactive libraries, micro frontends can subscribe to global state updates regardless of their underlying framework.
7. Handling State with Custom Events in Micro Frontends
One effective strategy for managing state across micro frontends, especially when they are built using different frameworks or technologies, is the use of Custom Events. Custom events allow micro frontends to broadcast and listen for changes in state without the need for a centralized state management system.
Custom events provide a way for loosely coupled micro frontends to communicate without direct dependencies, making it an ideal solution for maintaining independence between the individual micro frontends while still enabling shared behavior.
Example: Using Custom Events for Cross-Micro Frontend Communication
Here’s how you can use custom events to manage state updates between micro frontends:
Micro Frontend 1: Emit a Custom Event
// Micro frontend 1: Emit an event when user preferences are updated
const preferencesUpdateEvent = new CustomEvent('preferencesUpdated', {
detail: { theme: 'dark' },
});
window.dispatchEvent(preferencesUpdateEvent);
Micro Frontend 2: Listen for the Custom Event
// Micro frontend 2: Listen for the 'preferencesUpdated' event
window.addEventListener('preferencesUpdated', (event) => {
const { theme } = event.detail;
console.log('Theme updated to:', theme);
// Update UI or apply new theme
});
In this example, Micro Frontend 1 emits a custom event when the user changes their theme preference. Micro Frontend 2 listens for the preferencesUpdated
event and updates its own UI based on the new theme. This decouples the micro frontends from one another while ensuring that they can still share relevant state updates.
Benefits of Custom Events in Micro Frontends
Framework Agnostic: Custom events work across different frameworks and libraries, making them ideal for micro frontend architectures where different teams may be using different technologies.
Decoupled Communication: By broadcasting events instead of directly interacting with other micro frontends, each micro frontend remains independent, allowing for easier scaling and development.
Simplicity: Custom events are easy to implement and do not require complex state management libraries or tools, making them a lightweight solution for inter-micro frontend communication.
8. Synchronizing State Across Micro Frontends
In micro frontend architectures, synchronizing state between different micro frontends can be a challenge, especially when they need to work together to present a cohesive user experience. One practical solution is to use shared services or global event buses to coordinate state changes across micro frontends.
A shared service can act as a central authority for specific pieces of state, such as user authentication or shared configurations. Micro frontends can communicate with this service to get or update state as needed.
Example: Using a Shared Service for Synchronization
Let’s create a shared service for managing global user authentication state:
// Shared Auth Service
const AuthService = (() => {
let user = null;
const setUser = (newUser) => {
user = newUser;
// Emit an event to notify other micro frontends
const authEvent = new CustomEvent('userUpdated', { detail: user });
window.dispatchEvent(authEvent);
};
const getUser = () => user;
return { setUser, getUser };
})();
// Micro frontend 1: Set user state on login
AuthService.setUser({ name: 'John Doe', loggedIn: true });
// Micro frontend 2: Listen for updates to user state
window.addEventListener('userUpdated', (event) => {
console.log('User state updated:', event.detail);
});
In this example, AuthService
acts as a shared service responsible for managing the authentication state of the user. When the state is updated (e.g., when a user logs in), an event is emitted to notify other micro frontends. This allows them to synchronize and update their own behavior accordingly.
Conclusion: Striking the Right Balance
Managing state in a micro frontend architecture requires careful planning and a balance between local and global state. Each micro frontend should manage its own local state as much as possible, only lifting state to a global level when absolutely necessary. Techniques such as event-driven communication, URL-based state, web storage APIs, and cross-framework state management tools can help create a decoupled yet cohesive application.
The ultimate goal is to ensure that micro frontends remain independent and scalable while still allowing for shared state where needed. By adopting the strategies outlined in this article, you can ensure that your micro frontends maintain performance, consistency, and flexibility as your application grows.
At PixelFree Studio, we specialize in building scalable, high-performance web applications using modern architectures like micro frontends. If you’re looking to optimize state management in your micro frontend projects or need expert guidance on developing scalable frontend systems, contact us today to see how we can help!
Read Next: