You need a micro-frontend. Here's why?

You need a micro-frontend. Here's why?

Micro frontend for dummies

Preface:

First of all, what is a micro-frontend? Imagine you have super cool Lego blocks that you can use to build a house. You make a few squares and stack them on top of each other. Then you build a pyramid structure. Then you build a long cuboid like a chimney. Now, you assemble them to make it a house.

Later when you think of building a mansion with the same Lego blocks, you try to re-build a few squares and other shapes. But, wait a minute. You have built that already! So, if you had some doors/walls pre-built, wouldn't it save you a lot of time?

Now, imagine the Lego blocks like web components similar to div blocks, span elements, inputs, and buttons. Is it handy to have a pre-built customized component library that can save us a lot of time? At Lyzr, we build fast and ship fast. Now, if you're someone like me you can use module federation.

Below is a visual representation of micro-frontend architecture:

Necessity

Okay, but why not use some beautiful existing component libraries? Fortunately yes, but unfortunately the real problem is configuring them to fit into your new project. Are you someone who builds or maintains a lot of projects? Module federation is the answer to your problems.

Benefits:

The benefits outweigh the demerits of this approach. Let me explain:

⚡ Consistency in design with flexibility to change theme.

⚡ Build once and use it everywhere.

⚡ Framework agnostic. This means, that if your components are built using Svelte or Vue it will be compatible with React/Svelte and other frontend libraries.

⚡ Bring in multiple micro-frontends into a single app.

⚡ Most of all, it keeps the code super clean as most of the code is abstracted.

Demerits:

Please take this with a pinch of salt. No approach is 100% solution.

👎 Single point of failure (SPOF). This means a single bug in your component library can bring down your entire app.

👎 The shared code is dynamically fetched at runtime making the first load longer than usual on every reload.

👎 No type intellisense. I know, this is a deal breaker for a lot of us. But, I do have a tip to bypass this. Please stay with me till the end.

Sounds Interesting? Let's build one:

  • Step 1:

    • Build a host vite project.

    • Build a remote vite project

    • Make sure to change the project names in the package.json and the content in App.tsx to host and remote respectively.

    • Set the port according to your choice. I have fixed it as 5001 here.

        {
          "name": "host-application",
          "private": true,
          "version": "0.0.0",
          "type": "module",
          "homepage": ".",
          "scripts": {
            "dev": "vite --port 5001 --strictPort",
            "preview": "vite preview --port 5001 --host 0.0.0.0 --strictPort",
            "serve": "vite preview --port 5001 --host 0.0.0.0 --strictPort",
          },
          ...
        }
      
    • Remote project stores our components.

    • I'm assuming you're a developer who already knows how to build a vite project. If you don't know read how to create a vite project

  • Step 2:

    • Install vite plugin for module federation

    • Make sure to install in both projects

        npm install @originjs/vite-plugin-federation --save-dev
      
  • Step 3:

    • Configure remote project

        // vite.config.js
        import federation from "@originjs/vite-plugin-federation";
        export default {
            plugins: [
                federation({
                    name: 'lyzr-component-lib',
                    filename: 'remoteEntry.js',
                    // Modules to expose
                    exposes: {
                        './Button': './src/components/ui/Button.tsx',
                    },
                    shared: ['react', 'react-dom']
                })
            ]
        }
      
    • Create a button component. Build the remote project and run the preview

        npm run build; npm run preview
      
    • Configure host project

        // vite.config.js
        import federation from "@originjs/vite-plugin-federation";
        export default {
            plugins: [
                federation({
                    name: 'host-app',
                    remotes: {
                        // Same name as remote project but with camelCase
                        lyzrComponentLib: "http://localhost:5001/assets/remoteEntry.js",
                    },
                    shared: ['react', 'react-dom']
                })
            ]
        }
      
    • Add src/tsremote_declaration.ts

        declare module 'remoteApp/*'
      
  • Step 4:

    • In the host project, you can start using the component library.

        import RemoteButton from 'remoteApp/Button'
      
        const App: React.FC = () => {
           return (
             <div>
               <RemoteButton />
             </div>
           )
        }
      

Congratulations 🥳 ! You have created your own micro-frontend!

Best practices

Apart from common best practices, devs at Lyzr were able to improve the quality and pace of the development by following these:

📌 Build components using S.O.L.I.D principles.

📌 Test the components thoroughly.

📌 Type the props appropriately

📌 On the brighter side, this also provides utility functions.

How does it look?

Hope you're able to follow and achieve the desired output. If you're like me, you'd be very frustrated to realize the type of intelligence does not work. This approach still adds a few more steps to the process and is not backward compatible. But, it helps greatly in the long run as you only need to configure it once.

In your src/tsremote_declarations.ts file, update the component types as:

declare module 'remoteApp/Button' {
  import { ButtonHTMLAttributes, FC, ReactNode } from 'react'
  interface ButtonProps {
    variant?:
      | 'default'
      | 'primary'
      | 'secondary'
      | 'outline'
      | 'ghost'
      | 'destructive'
      | 'chat'
      | 'link'
    size?: 'sm' | 'lg' | 'default'
    asChild?: boolean
    startIcon?: ReactNode
    endIcon?: ReactNode
    loading?: boolean
  }
  const Button: FC<ButtonProps & ButtonHTMLAttributes<HTMLButtonElement>>
  export { Button }
}

declare module 'remoteApp/*'

And, et viola! 🎉

Thank you for reading the full blog post! 🙏

Hope you've enjoyed reading and learned something new today.