pwshub.com

Practical guidance between vertical and horizontal micro-frontends

Micro-frontends, like microservices in backend development, divide frontend applications into modular, self-contained components that can be independently developed, tested, and deployed by different teams in large projects.

This image features the iconic React logo—a blue atom-like symbol with a central dot and orbiting curves—superimposed on a background of a city skyline. The city appears to be shrouded in soft pink and purple hues, giving the image a modern, tech-focused atmosphere. This image symbolizex the use of React for building modular, scalable frontend architectures. React's component-based approach aligns with the principles of micro-frontends, where different parts of the user interface are independently developed and managed, similar to how microservices operate on the backend. The cityscape in the background represents the larger, complex web applications that can benefit from a structured, flexible frontend framework like React.

This article showcases advanced coding strategies for implementing vertical and horizontal micro-frontends through highlighting their specific use cases and providing original code examples to help you choose the most effective approach for optimizing your project’s scalability and maintainability.

What are micro-frontends?

Micro-frontends apply microservice principles to web design by dividing a large, monolithic frontend into smaller, independent components, similar to building blocks.

Read how micro-frontends apply microservice concepts to enhance scalability, flexibility, and team autonomy in web development by breaking down large frontends into independently manageable units.

State management across different micro-frontend components can become very challenging. Examples of such components are used in the code snippet below:
UserMicroFrontend and CartMicroFrontend.

Here, the ParentApp components are in charge of the shared state management (userData and cartData) and pass it down as props to each micro-frontend:


// ParentApp.js
import React, { useState, useEffect } from 'react';
import CartMicroFrontend from './CartMicroFrontend';
import UserMicroFrontend from './UserMicroFrontend';
const ParentApp = () => {
  const [userData, setUserData] = useState(null);
  const [cartData, setCartData] = useState([]);
  // Simulating fetching user data
  useEffect(() => {
    fetchUserData().then(data => setUserData(data));
  }, []);
  // Handler to update the cart
  const updateCart = (newItem) => {
    setCartData([...cartData, newItem]);
  };
  return (
    <div>
      <h1>Parent Application</h1>
      <UserMicroFrontend userData={userData} />
      <CartMicroFrontend cartData={cartData} onAddToCart={updateCart} />
    </div>
  );
};
const fetchUserData = async () => {
  // Simulate API call to fetch user data
  return { name: 'John Doe', id: 123 };
};
export default ParentApp;
// UserMicroFrontend.js
import React from 'react';
const UserMicroFrontend = ({ userData }) => {
  return (
    <div>
      <h2>User Information</h2>
      {userData ? (
        <p>{`Welcome, ${userData.name}`}</p>
      ) : (
        <p>Loading user data...</p>
      )}
    </div>
  );
};
export default UserMicroFrontend;
// CartMicroFrontend.js
import React from 'react';
const CartMicroFrontend = ({ cartData, onAddToCart }) => {
  const handleAddToCart = () => {
    const newItem = { id: Math.random(), name: 'New Product' };
    onAddToCart(newItem);
  };
  return (
    <div>
      <h2>Shopping Cart</h2>
      <ul>
        {cartData.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
      <button onClick={handleAddToCart}>Add Item</button>
    </div>
  );
};
export default CartMicroFrontend;

As the application scales, managing shared state between different micro-frontends can become increasingly complex, especially when these micro-frontends are independently deployed and need to communicate with each other.

To address this challenge, consider implementing a global state management solution, such as Redux or the Context API, or using event-driven architectures to facilitate **cross-micro-frontend communication, depending on the complexity and specific needs of your application.

Possible challenges with managing shared state in micro-frontends

Complexity — While micro-frontends offer significant benefits, they introduce complexity in managing shared state across multiple independent modules.

Each micro-frontend might have its own state, requiring careful planning and architecture to ensure consistent state management and avoid conflicts, particularly when modules need to share data or interact with each other.

Integration — Since each micro-frontend is developed independently, integrating them into a cohesive user experience poses a challenge. This involves establishing common protocols for state sharing and inter-module communication, ensuring that all modules can work together seamlessly without compromising the overall functionality.

Coordination in large teams — In large organizations where different teams are responsible for different parts of an application, keeping everyone aligned on state management becomes difficult.

Ensuring a consistent user experience across the application requires tight coordination, clear guidelines, and effective project management to maintain consistency in how state is managed and shared among micro-frontends.

Client-side composition routing

A key feature of vertical micro-frontends is client-side composition. Unlike server-side rendering, where the server assembles the page, client-side composition allows the application shell to load and manage micro-frontends dynamically.

This approach is particularly useful for single-page applications (SPAs), where a seamless and responsive user experience is paramount.

The routing in a vertical split is typically handled in two layers:

  • Global routing — Managed by the application shell, this routing layer is responsible for navigating between different micro-frontends. For example, it might route from the user profile section to the shopping cart, ensuring a smooth transition between major sections of the application
  • Local routing — Each micro-frontend manages its own internal navigation. For instance, within the user profile micro-frontend, routes might exist for editing user information, viewing order history, etc.. This separation allows teams to have full control over the navigation and logic within their domain, ensuring that changes in one area do not inadvertently affect others

Application shell implementation example

Here’s a simple example of how the application shell might be implemented in a React-based e-commerce application:


// AppShell.jsx
import React from 'react';
import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom';
import ProductListing from './ProductListing';
import ShoppingCart from './ShoppingCart';
import UserProfile from './UserProfile';
function AppShell() {
  return (
    <Router>
      <Switch>
        <Route path="/products" component={ProductListing} />
        <Route path="/cart" component={ShoppingCart} />
        <Route path="/profile" component={UserProfile} />
        <Redirect from="/" to="/products" />
      </Switch>
    </Router>
  );
}
export default AppShell;

In this instance, the AppShell.jsx component manages routing to various micro-frontends like ProductListing, ShoppingCart, UserProfile, etc.. Using React Router, it allows switching between them depending on the URL path.

The Switch component ensures that only one route is rendered at once while the Redirect component creates a default route leading to the product listing page.

Product listing micro-frontend

The ProductListing component is a micro-frontend responsible for displaying a list of products. It fetches product data from an API and renders it for the user:


// ProductListing.jsx
import React, { useState, useEffect } from 'react';
function ProductListing() {
  const [products, setProducts] = useState([]);
  useEffect(() => {
    fetch('/api/products')
      .then((response) => response.json())
      .then((data) => setProducts(data));
  }, []);
  return (
    <div>
      <h1>Product Listing</h1>
      <ul>
        {products.map((product) => (
          <li key={product.id}>{product.name}</li>
        ))}
      </ul>
    </div>
  );
}
export default ProductListing;

Here, ProductListing.jsx manages the state of the products using the useState hook. The useEffect hook fetches product data from the server when the component mounts, updating the product list.

This micro-frontend focuses solely on product-related functionality, making it easy to maintain and scale independently.

Shopping cart micro-frontend

The ShoppingCart component handles the shopping cart functionality, allowing users to view and manage their cart items:


// ShoppingCart.jsx
import React, { useState } from 'react';
function ShoppingCart() {
  const [cart, setCart] = useState([]);
  const handleAddToCart = (product) => {
    setCart([...cart, product]);
  };
  return (
    <div>
      <h1>Shopping Cart</h1>
      <ul>
        {cart.map((item, index) => (
          <li key={index}>{item.name}</li>
        ))}
      </ul>
      <button onClick={() => handleAddToCart({ name: 'Example Product' })}>
        Add Example Product
      </button>
    </div>
  );
}
export default ShoppingCart;

In ShoppingCart.jsx, the useState hook manages the cart’s state, storing the list of items added by the user. The handleAddToCart function adds new items to the cart. This component provides a focused and isolated experience for managing shopping cart operations, further emphasizing the benefits of a vertical micro-frontend approach.

User profile micro-frontend

The UserProfile component displays and manages user profile information:


// UserProfile.jsx
import React, { useState, useEffect } from 'react';
function UserProfile() {
  const [user, setUser] = useState(null);
  useEffect(() => {
    fetch('/api/user')
      .then((response) => response.json())
      .then((data) => setUser(data));
  }, []);
  if (!user) return <div>Loading...</div>;
  return (
    <div>
      <h1>User Profile</h1>
      <p>Name: {user.name}</p>
      <p>Email: {user.email}</p>
    </div>
  );
}
export default UserProfile;

The UserProfile.jsx component uses the useState hook to manage the user data state. The useEffect hook fetches user information from the server, updating the component’s state when the data is received. This micro-frontend handles user-specific tasks, such as displaying and editing profile information, independently from other parts of the application.


More great articles from LogRocket:

  • Don't miss a moment with The Replay, a curated newsletter from LogRocket
  • Learn how LogRocket's Galileo cuts through the noise to proactively resolve issues in your app
  • Use React's useEffect to optimize your application's performance
  • Switch between multiple versions of Node
  • Discover how to use the React children prop with TypeScript
  • Explore creating a custom mouse cursor with CSS
  • Advisory boards aren’t just for executives. Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.

Client-side composition

Client-side composition in a horizontal split architecture typically involves loading multiple micro-frontends into a single view. An application shell manages these components, allowing each team to deliver their functionality independently.

This approach is suitable for applications with high traffic, as caching strategies can be efficiently implemented using a CDN.

Here is a React Code Example: Application Shell:


import React from 'react';
import Header from './Header';
import Footer from './Footer';
import Catalog from './Catalog';
import VideoPlayer from './VideoPlayer';
function AppShell() {
  return (
    <div className="app-shell">
      <Header />
      <div className="main-content">
        {/* Load micro-frontends */}
        <Catalog />
        <VideoPlayer />
      </div>
      <Footer />
    </div>
  );
}
export default AppShell;
import React from 'react';
import Header from './Header';
import Footer from './Footer';
import Catalog from './Catalog';
import VideoPlayer from './VideoPlayer';
function AppShell() {
  return (
    <div className="app-shell">
      <Header />
      <div className="main-content">
        {/* Load micro-frontends */}
        <Catalog />
        <VideoPlayer />
      </div>
      <Footer />
    </div>
  );
}
export default AppShell;

In the example above, the AppShell component serves as the application shell, loading the Catalog and VideoPlayer micro-frontends. Each component is developed by a different team, allowing for independent development and deployment.

Edge-side composition

Edge-side composition is useful for projects with high traffic and static content. By leveraging a CDN, the application can efficiently handle scalability challenges. This approach can be beneficial for online catalogs, news websites, and other applications where fast content delivery is crucial.

Here is a React code example — edge-side composition with static content:


import React from 'react';
import ProductList from './ProductList';
import AdBanner from './AdBanner';
function CatalogPage() {
  return (
    <div className="catalog-page">
      <ProductList />
      <AdBanner />
    </div>
  );
}
export default CatalogPage;

In this example, we can still compose the CatalogPage component at CDN level where it gets micro-frontends ProductList and AdBanner. The data for these components are served statically, so the load times will still be fast and a great user experience.

Server-side composition

Server-side composition controls the final output i.e. when you need maximum control over what users see, especially for SEO-critical sites like online stores or news platforms. This method allows for better performance metrics, as the server can render the entire page before sending it to the client.

Here is a React code example — server-side composition:


import React from 'react';
import express from 'express';
import { renderToString } from 'react-dom/server';
import Header from './Header';
import ProductDetails from './ProductDetails';
import Footer from './Footer';
const app = express();
app.get('/product/:id', (req, res) => {
  const productDetailsHtml = renderToString(<ProductDetails productId={req.params.id} />);
  const html = `
    <!DOCTYPE html>
    <html>
      <head>
        <title>Product Details</title>
      </head>
      <body>
        <div id="app">
          <div>${renderToString(<Header />)}</div>
          <div>${productDetailsHtml}</div>
          <div>${renderToString(<Footer />)}</div>
        </div>
      </body>
    </html>
  `;
  res.send(html);
});
app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

In this server-side composition example, the server renders the Header, ProductDetails, and Footer components into an HTML string. This HTML is then sent to the client, ensuring that the page is fully rendered before it reaches the user’s browser, which improves load times and SEO.

Micro-frontend communication

A key challenge in horizontal-split architectures is managing communication between micro-frontends. Unlike components, micro-frontends should not share a global state, as this would tightly couple them, defeating the purpose of modularization. Instead, event-driven architectures are recommended.

Here is a React code example — EventEmitter for communication:


// EventEmitter.js
import { EventEmitter } from 'events';
export const eventEmitter = new EventEmitter();
// Catalog.js
import React from 'react';
import { eventEmitter } from './EventEmitter';
function Catalog() {
  const selectProduct = (productId) => {
    eventEmitter.emit('productSelected', productId);
  };
  return (
    <div>
      {/* Product selection logic */}
      <button onClick={() => selectProduct(1)}>Select Product 1</button>
    </div>
  );
}
export default Catalog;
// VideoPlayer.js
import React, { useEffect } from 'react';
import { eventEmitter } from './EventEmitter';
function VideoPlayer() {
  useEffect(() => {
    const handleProductSelected = (productId) => {
      console.log('Product selected:', productId);
      // Handle product selection logic
    };
    eventEmitter.on('productSelected', handleProductSelected);
    return () => {
      eventEmitter.off('productSelected', handleProductSelected);
    };
  }, []);
  return (
    <div>
      {/* Video player logic */}
      <h2>Video Player</h2>
    </div>
  );
}
export default VideoPlayer;

In this code snippet, EventEmitter is used to manage communication between the Catalog and VideoPlayer micro-frontends. When a product is selected in the Catalog component, an event is emitted. The VideoPlayer component listens for this event and reacts accordingly, maintaining a loose coupling between the two micro-frontends.

Vertical vs. horizontal micro-frontends

AspectVertical micro-frontendsHorizontal micro-frontends
StructureFull-stack feature ownership (vertical slices)Layered separation (UI, business logic, data services)
DevelopmentIndependent development per featureSpecialized teams per layer
DeploymentFeature-specific deploymentsLayer-specific deployments
ScalabilityEasy to scale individual featuresCoordination needed across teams
Team OrganizationCross-functional teamsSpecialized teams focusing on specific layers
State ManagementSelf-contained states within each featureShared state across different layers
ComplexityEasier to manage feature-specific complexityMore complex due to shared components and dependencies
ExamplesE-commerce sites with independent categoriesPlatforms with unified UI and diverse backend services.

Conclusion

While micro-frontends offer a powerful approach to scaling and managing large web applications by breaking them into smaller, independently developed modules, they also introduce challenges that must be carefully managed.

The complexity of shared state management, the intricacies of integrating independently developed modules, and the need for coordinated efforts across large teams are all significant considerations.

However, with proper planning, the implementation of robust state management solutions, and effective project coordination, these challenges can be overcome, allowing organizations to fully leverage the benefits of micro-frontends and deliver a seamless, scalable user experience.

Source: blog.logrocket.com

Related stories
1 week ago - A healthcare virtual assistant (VA) is an AI tool that supports medical professionals with administrative services, such as scheduling appointments, making phone calls, managing email accounts, and more. The primary goals of a VA are to...
1 month ago - As a product manager, you need to strike the right balance between high-level strategic thinking and detailed execution. The post What is the ladder of abstraction? appeared first on LogRocket Blog.
1 month ago - This article aims to celebrate the power of introversion in UX research and design. Victor Yocco debunks common misconceptions, explores the unique strengths introverted researchers and designers bring to the table, and offers practical...
1 month ago - Small businesses offer entrepreneurs financial independence and personal fulfillment. They open the pathway to positively impact a community. With many side business ideas available, choosing an innovative business concept ensures its...
1 month ago - Anticipatory design, powered by Artificial Intelligence (AI), Machine learning (ML), and Big Data (BD), promises to transform user experiences by predicting and fulfilling needs before users even express them. While this proactive...
Other stories
2 hours ago - Fixes 41 bugs (addressing 595 👍). node:http2 server and gRPC server support, ca and cafile support in bun install, Bun.inspect.table, bun build --drop, iterable SQLite queries, iterator helpers, Promise.try, Buffer.copyBytesFrom, and...
7 hours ago - This guide provides a foundational understanding of Redux and why you should use it for state management in a React app. The post Understanding Redux: A tutorial with examples appeared first on LogRocket Blog.
9 hours ago - Discover some of the best Node.js web scraping libraries, including Axios and Superagent, and techniques for how to use them. The post The best Node.js web scrapers for your use case appeared first on LogRocket Blog.
12 hours ago - Infinite runner games have been a favorite for gamers and developers alike due to their fast-paced action and replayability. These games often feature engaging mechanics like endless levels, smooth character movement, and dynamic...
14 hours ago - Yesterday, Elizabeth Siegle, a developer advocate for CLoudflare, showed off a really freaking cool demo making use of Cloudflare's Workers AI support. Her demo made use of WNBA stats to create a beautiful dashboard that's then enhanced...