Scaling React Applications with Microfrontends and Vite

β€’ By Daian Scuarissi
reactmicrofrontendsvitearchitecturescalingweb-development

Microfrontends are revolutionizing how we build and deploy web applications, enabling teams to work independently while creating cohesive user experiences. In this comprehensive guide, we'll explore how to create a production-ready microfrontend architecture using React and Vite, complete with Hot Module Replacement (HMR) for an optimal development experience.

What Are Microfrontends?

Imagine building a large e-commerce platform where different teams handle the product catalog, shopping cart, user authentication, and checkout process. Traditional monolithic frontends require all teams to coordinate releases, use the same technology stack, and deploy together. Microfrontends solve this by allowing each team to:

Why React + Vite for Microfrontends?

React provides the component-based architecture perfect for microfrontend boundaries, while Vite offers lightning-fast development with excellent module federation support. This combination delivers:

Prerequisites and Versions

This tutorial uses the following versions for reproducible results:

Setting Up Your Microfrontend Architecture

Let's build a practical example with a host application that consumes components from a remote application.

Step 1: Project Structure

Create the following directory structure:

my-microfrontend/
β”œβ”€β”€ host-app/          # Consumer application
└── remote-app/        # Provider application

Step 2: Create the Remote Application

The remote app will expose reusable components to other applications.

# Create and setup remote app
npm create vite@7.1.2 remote-app -- --template react-ts
cd remote-app
npm install @originjs/vite-plugin-federation@1.4.1 --save-dev

Configure remote-app/vite.config.ts:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import federation from '@originjs/vite-plugin-federation';
 
export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'remote_app',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/components/Button',
        './Header': './src/components/Header',
      },
      shared: {
        'react': { 
          singleton: true,
          requiredVersion: '^19.1.1'
        },
        'react-dom': { 
          singleton: true,
          requiredVersion: '^19.1.1'
        }
      },
    }),
  ],
  build: {
    target: 'esnext',
    minify: false,
    cssCodeSplit: false,
  },
  server: {
    port: 5001,
    strictPort: true,
    cors: true,
  },
});

Create shared components in remote-app/src/components/Button.tsx:

import React from 'react';
 
interface ButtonProps {
  text: string;
  onClick?: () => void;
}
 
const Button: React.FC<ButtonProps> = ({ text, onClick }) => {
  return (
    <button
      onClick={onClick}
      className='px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600'
    >
      {text}
    </button>
  );
};
 
export default Button;

And remote-app/src/components/Header.tsx:

import React from 'react';
 
const Header: React.FC = () => {
  return (
    <header className='bg-gray-800 text-white p-4'>
      <h1 className='text-2xl font-bold'>Remote Header Component</h1>
      <p>This header is loaded from the remote application!</p>
    </header>
  );
};
 
export default Header;

Update package.json scripts:

{
  "scripts": {
    "dev": "vite",
    "dev:fed": "vite build --watch --mode dev && vite preview --port 5001 --strictPort",
    "build": "tsc -b && vite build",
    "preview": "vite preview --port 5001 --strictPort",
    "serve": "npm run build && npm run preview"
  }
}

Step 3: Create the Host Application

The host app will consume components from the remote application.

# Create and setup host app
npm create vite@7.1.2 host-app -- --template react-ts
cd host-app
npm install @originjs/vite-plugin-federation@1.4.1 --save-dev

Configure host-app/vite.config.ts:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import federation from '@originjs/vite-plugin-federation';
 
export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'host_app',
      remotes: {
        remote_app: 'http://localhost:5001/assets/remoteEntry.js',
      },
      shared: {
        'react': { 
          singleton: true,
          requiredVersion: '^19.1.1'
        },
        'react-dom': { 
          singleton: true,
          requiredVersion: '^19.1.1'
        }
      },
    }),
  ],
  build: {
    target: 'esnext',
    minify: false,
    cssCodeSplit: false,
  },
  server: {
    port: 5000,
    strictPort: true,
    cors: true,
  },
});

Create type definitions in host-app/src/types/remote.d.ts:

declare module 'remote_app/Button' {
  import React from 'react';
 
  interface ButtonProps {
    text: string;
    onClick?: () => void;
  }
 
  const Button: React.FC<ButtonProps>;
  export default Button;
}
 
declare module 'remote_app/Header' {
  import React from 'react';
 
  const Header: React.FC;
  export default Header;
}

Create a component wrapper in host-app/src/components/RemoteComponentWrapper.tsx:

import React, { Suspense } from 'react';
 
const RemoteHeader = React.lazy(() => import('remote_app/Header'));
const RemoteButton = React.lazy(() => import('remote_app/Button'));
 
const LoadingSpinner = () => (
  <div className='flex justify-center p-4'>
    <div className='animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900'></div>
  </div>
);
 
export const RemoteComponentWrapper = () => {
  return (
    <div className='p-4 space-y-4'>
      <Suspense fallback={<LoadingSpinner />}>
        <RemoteHeader />
      </Suspense>
 
      <div className='mt-4'>
        <Suspense fallback={<LoadingSpinner />}>
          <RemoteButton
            text='Click me!'
            onClick={() => alert('Remote button clicked!')}
          />
        </Suspense>
      </div>
    </div>
  );
};

Update host-app/src/App.tsx:

import React from 'react';
import viteLogo from '/vite.svg';
import './App.css';
import { RemoteComponentWrapper } from './components/RemoteComponentWrapper';
 
function App() {
  return (
    <div className='px-6 py-8'>
      <div className='flex justify-center items-center mb-8'>
        <img src={viteLogo} alt='Vite' className='h-12' />
      </div>
 
      <h1 className='text-3xl font-bold text-center mb-4'>
        🏠 Host Application
      </h1>
 
      <p className='text-center text-gray-600 mb-8'>
        Welcome to the Host application! Below are components loaded from the
        remote application:
      </p>
 
      <RemoteComponentWrapper />
    </div>
  );
}
 
export default App;

The Magic: Development Workflow with HMR

Here's where the real power shines. To enable full Hot Module Replacement across microfrontends:

Terminal 1 - Remote App (Build Watcher):

cd remote-app
npm run serve

Terminal 2 - Remote App (HMR Builder):

cd remote-app
npm run dev:fed

Terminal 3 - Host App:

cd host-app
npm run dev

Now visit http://localhost:5000 and watch the magic happen! When you modify any component in the remote app, you'll see:

  1. Instant rebuild of the remote components
  2. Automatic reload in the host application
  3. Preserved state where possible
  4. Zero configuration required

Try changing the button text in remote-app/src/components/Button.tsx and watch it update immediately in the host app!

Best Practices for Production

1. Error Boundaries Are Essential

Wrap remote components in error boundaries to prevent cascading failures:

import React, { Component, ReactNode } from 'react';
 
interface Props {
  children: ReactNode;
  fallback?: ReactNode;
}
 
interface State {
  hasError: boolean;
}
 
class MicrofrontendErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }
 
  static getDerivedStateFromError(): State {
    return { hasError: true };
  }
 
  render() {
    if (this.state.hasError) {
      return (
        this.props.fallback || (
          <div className='p-4 text-red-600 border border-red-300 rounded'>
            Something went wrong loading this component.
          </div>
        )
      );
    }
 
    return this.props.children;
  }
}

2. Shared Dependencies Strategy

Be strategic about what you share:

// Good: Share major frameworks with specific versions
shared: {
  'react': { 
    singleton: true,
    requiredVersion: '^19.1.1'
  },
  'react-dom': { 
    singleton: true,
    requiredVersion: '^19.1.1'
  },
  // Share design system components
  '@your-company/ui-components': { 
    singleton: true,
    requiredVersion: '^2.1.0'
  }
}
 
// Avoid: Sharing too many small utilities
// This can lead to version conflicts

3. Environment-Specific Configuration

Use environment variables for different deployment stages:

const remoteUrl =
  process.env.NODE_ENV === 'production'
    ? 'https://remote.yourcompany.com/assets/remoteEntry.js'
    : 'http://localhost:5001/assets/remoteEntry.js';

4. Performance Considerations

Troubleshooting Common Issues

Issue: Remote components not loading

Solution: Verify CORS settings and ensure remote app is running on the correct port.

Issue: Type errors with remote imports

Solution: Ensure your remote.d.ts file matches the actual component interfaces.

Issue: Styles not applying correctly

Solution: Configure cssCodeSplit: false in build options to prevent CSS conflicts.

Advanced Patterns

Bi-directional Communication

Sometimes you need microfrontends to communicate:

// Using custom events
const sendMessage = (data: any) => {
  window.dispatchEvent(
    new CustomEvent('microfrontend:message', {
      detail: data,
    })
  );
};
 
// In another microfrontend
useEffect(() => {
  const handleMessage = (event: CustomEvent) => {
    console.log('Received:', event.detail);
  };
 
  window.addEventListener('microfrontend:message', handleMessage);
  return () =>
    window.removeEventListener('microfrontend:message', handleMessage);
}, []);

Route-Based Microfrontends

Combine with React Router for route-based loading:

const RemoteApp = React.lazy(() => import('remote_app/App'));
 
<Route
  path='/remote/*'
  element={
    <Suspense fallback={<Loading />}>
      <RemoteApp />
    </Suspense>
  }
/>;

Conclusion

Microfrontends with React and Vite offer a powerful solution for scaling frontend development. The combination provides:

Start small with a simple host-remote setup like we've built, then gradually adopt more advanced patterns as your application grows. The key is finding the right balance between independence and integration for your specific use case.

Remember, microfrontends are not a silver bulletβ€”they add complexity in exchange for scalability. Use them when you have multiple teams, complex applications, or need independent deployment cycles.

Ready to build your own microfrontend architecture? The code from this tutorial is available as a complete working example. Happy coding! πŸš€


Have you implemented microfrontends in your projects? Share your experiences and challenges in the comments below!