top of page
Search

Micro Frontends with React and Next.js: A Practical Guide

Micro Frontends with React and Next.js: A Practical Guide


Micro frontends have emerged as a powerful architectural pattern for building large, complex web applications. By breaking down the frontend into smaller, independent units, teams can work autonomously, deploy more frequently, and choose the best technology for each part of the UI. This article explores how to implement micro frontends using React and Next.js, focusing on the container pattern.


What are Micro Frontends?


Micro frontends are an architectural approach where a frontend application is composed of smaller, independent applications.  Each of these smaller applications, often called "micro frontends," is owned and managed by a separate team. They are then integrated together to form the final user interface.


Why use Micro Frontends?


*Independent Teams:** Different teams can work on different micro frontends without blocking each other.

*Technology Diversity:** Teams can choose the most suitable technology for their micro frontend (though consistency often simplifies maintenance).

*Improved Scalability:**  Large frontend projects become more manageable.

*Easier Upgrades:** Individual micro frontends can be upgraded or replaced without rewriting the entire application.


React and Next.js in Micro Frontends


*React:**  React's component-based architecture makes it ideal for building individual micro frontends.

*Next.js:** Next.js can be used as a container to orchestrate and integrate the different micro frontends.


The Container Pattern with Next.js


In this pattern, a Next.js application acts as the "shell" or "orchestrator."  It's responsible for:


1. Routing:  Mapping URLs to specific micro frontends.

2. Integration: Loading and rendering the micro frontends on the page.

3. Shared Layout: Providing common UI elements (header, footer, etc.).

4. Communication (if needed): Facilitating communication between micro frontends.


Example Implementation (TypeScript)


Let's illustrate with a simplified example. We'll have a "Products" micro frontend and a Next.js container.


1. The "Products" Micro Frontend (React with minimal setup for demonstration)


```typescript jsx

// products-app.tsx (Simplified - in a real app, this would be a more complete React app)

import React from 'react';

import ReactDOM from 'react-dom/client';


interface ProductsProps {

  mountPoint: HTMLElement;

}


const Products: React.FC<ProductsProps> = ({ mountPoint }) => {

  const products = ['Product A', 'Product B', 'Product C'];


  return (

    <div>

      <h2>Products</h2>

      <ul>

        {products.map((product) => (

          <li key={product}>{product}</li>

        ))}

      </ul>

    </div>

  );

};



export const mount = (el: HTMLElement) => {

  const root = ReactDOM.createRoot(el);

  root.render(<Products mountPoint={el} />);

};


// Expose mount function globally (for simplicity in this example) - consider a more robust approach in production

(window as any).mountProducts = mount;


// In a real micro frontend, you'd likely use a bundler (Webpack, Parcel) and expose the mount function in a more structured way.

```


2. The Next.js Container


```typescript jsx

// pages/products.tsx (Next.js page)

import { useEffect } from 'react';


const ProductsPage = () => {

  useEffect(() => {

    const loadMicroFrontend = async () => {

      // In a real app, you'd fetch this URL dynamically

      const microFrontendUrl = '/products-app.js'; // Path to your built micro frontend


      try {

        const script = document.createElement('script');

        script.src = microFrontendUrl;

        script.async = true;


        script.onload = () => {

          // Access the mount function from the micro frontend (make sure it's exposed)

          const mountProducts = (window as any).mountProducts;

          if (mountProducts) {

            const productsContainer = document.getElementById('products-container');

            if (productsContainer) {

              mountProducts(productsContainer);

            } else {

              console.error("Products container not found");

            }

          } else {

            console.error("Mount function not found in micro frontend");

          }

        };


        script.onerror = () => {

          console.error("Failed to load micro frontend");

        };


        document.body.appendChild(script);


      } catch (error) {

        console.error("Error loading micro frontend:", error);

      }

    };


    loadMicroFrontend();


    return () => {

        // Cleanup: remove the script tag when the component unmounts (important!)

        const script = document.querySelector(`script[src="${microFrontendUrl}"]`);

        if (script) {

          script.remove();

        }

      };

  }, []);


  return (

    <div>

      <h1>Products</h1>

      <div id="products-container"></div>

    </div>

  );

};


export default ProductsPage;

```


Explanation:


*Products Micro Frontend:** This is a simple React application. The key is the `mount` function, which takes a DOM element as an argument and renders the `Products` component into it.  This is how the container will inject the micro frontend.  In a real-world scenario, you would use a build tool like Webpack or Parcel to create a bundle (`products-app.js`) from this code and expose the `mount` function in a more controlled manner (e.g., using a global variable is just for simplicity in this example).

*Next.js Container:** The `ProductsPage` component uses `useEffect` to dynamically load the micro frontend's JavaScript file.  Once loaded, it accesses the `mountProducts` function (which we've made globally available for this example) and calls it, passing in the `products-container` element.


Key Improvements and Considerations for Production:


*Robust Micro Frontend Loading:** Instead of relying on a global variable and a direct script tag, use a more structured approach for loading the micro frontend's JavaScript bundle. Consider using Webpack's Module Federation, or a dedicated micro frontend loading library.

*Error Handling:** Implement proper error handling for loading and mounting the micro frontend.

*Communication:** For communication between micro frontends, consider using a shared event bus, a message queue, or a state management library.

*Build Process:** Set up a build process (with Webpack, Parcel, etc.) for each micro frontend to create optimized bundles.

*Deployment:** Deploy each micro frontend independently.

*CSS Isolation:** Ensure that CSS styles from different micro frontends don't clash. Techniques like CSS Modules or Shadow DOM can help.

*Shared Dependencies:** Carefully manage shared dependencies between micro frontends to avoid duplication and version conflicts.


This article provides a basic foundation for building micro frontends with React and Next.js.  Remember that micro frontend architectures introduce complexity, so careful planning and consideration of the specific needs of your project are crucial.  The example provided is simplified for demonstration; a production-ready implementation would require more robust handling of module loading, communication, and dependency management.

 
 
 

Recent Posts

See All
What we can learn from cats

That's a fascinating observation, and you've touched upon something quite profound about the apparent inner peace that some animals seem...

 
 
 
Post: Blog2_Post

Subscribe Form

Thanks for submitting!

©2020 by LearnTeachMaster DevOps. Proudly created with Wix.com

bottom of page