The way that we develop applications has changed a lot over the past few years. With new frameworks being released daily, having a flexible tech-stack, for example, building some parts of your app in React and others in Vue, is highly beneficial when working across teams.
Micro-frontends allow us to build web apps that are developed and deployed separately but work as a single app. Micro-frontends are one common approach to achieving greater flexibility, allowing teams to merge components built in different frameworks or libraries. In production, there are several different ways to implement micro-frontends.
In this tutorial, we’ll explore a few methods for building micro-frontends. Then we’ll look into Fronts.js in-depth, a progressive micro-frontend framework for building web applications. Let’s get started!
First, let’s review two common approaches to building micro-frontends. web
The single-spa router uses a single-spa root config
file, which holds information like shared dependencies, as well as several individual SPAs packaged into modules that communicate with each other and single-spa via APIs.
Another popular solution is webpack’s Module Federation, which was introduced as one of the core features in webpack v.5. Module Federation solves the problem of code sharing by allowing applications to load code from each other. However, Module Federation is a bit verbose and lacks the option for customization.
In comparison to other micro-frontend frameworks, Fronts provides a more complete and fully-targeted implementation, offering several unique benefits like support for nested micro-frontends. A nested micro-frontend is a frontend built with one codebase that is placed inside another frontend built with a different codebase. Nested micro-frontends allow a parent page to hold several different child pages that could each potentially be built using a different repository.
Additionally, Fronts offers cross-framework support, meaning developers are not restricted from using any particular tech stack. Cross-framework support also allows developers to build a single page using several different technologies. Code splitting and lazy loading are two additional popular features included in Fronts, which allow different apps that are built using Fronts to import different Fronts apps as modules.
We can hook into the lifecycle methods provided by Fronts apps and perform the required operations. Finally, the Fronts API is somewhat generic and framework agnostic, meaning we can apply it to a wide variety of use cases.
Fronts is progressive in nature, meaning it supports both Module Federation and non-module-federation. An application that starts off in a normal mode without Module Federation can progressively shift into Module Federation.
In addition to Module Federation, Fronts offers a version control mode that allows version management of the different component applications.
As we mentioned before, the Fronts API is pretty straightforward. However, it’s worth taking an in-depth look at its code. Fronts includes three different loaders, selecting the correct one based on the requirement.
When we need to isolate the CSS from other applications, we use the useWebComponents()
API. For applications that don’t require us to isolate the CSS from the other applications, we can use the useApp()
loader, however, doing so is optional on an opt-in basis. Lastly, we use the useIframe()
loader when we need to isolate both CSS and JavaScript from the rest of the application.
In addition to these three loaders, Fronts also includes two APIs for Module Federation. createWebpackConfig()
is a wrapper function that takes in the original webpack config and returns the updated webpack config that supports Module Federation. To generate the webpack config for Module Federation, simply call createWebpackConfig()
.
getMeta()
is a tool used to get the full picture of the dependency map. Calling getMeta()
returns a JSON of the entire monorepo structure, along with all the component apps and their dependencies.
Finally, Fronts provides APIs for communicating among the different component micro-frontends. globalTransport.listen
helps configure global event listeners by providing a string event and a listener that is triggered when that event is fired.
The globalTransport.emit
function emits events that the previous function will listen to. It takes a string event name as the first argument and its value as the second argument.
Now that we understand the essentials of Fronts, let’s run through an example to understand Fronts in greater depth.
For our demo, we’ll work on top of the official fronts-example repo. The container ecommerce site is the parent, and the products page is the child. To follow along, you can access the completed example repo. It uses Chakra UI for its UI components and has a hardcoded products.json
file along with a dummy cart.
Our final micro-frontend example will look like the image below:
To run the application, clone the repository, then run the following command:
yarn install
Next, run the code below:
start
To play around with and test the application, visit localhost.
The example repo above contains two full-fledged Fronts projects inside of the packages
directory. Keep in mind that it’s not necessary to place the two projects inside of the same monorepo. Our application would work exactly the same way if each one were placed in its own separate repository.
The folder structure of each individual app looks roughly like the following code segment:
|- public |- index.html |- src |- App.jsx |- index.jsx |- styles.css |- bootstrap.tsx |- .babelrc |- webpack.config.js |- site.json
Let’s look at each specific file in greater detail.
bootstrap.tsx
As the name suggests, we’ll use the bootstrap.tsx
file to help us bootstrap our Fronts application when it is loaded in the browser:
export default function render(element: HTMLElement | null) { ReactDOM.render(<App />, element); return () => { ReactDOM.unmountComponentAtNode(element!); }; } boot(render, document.getElementById('root'));
Notice how the app is rendered inside of a render function, then that render function is supplied to the boot
method provided by the Fronts library.
Inside of the function, the React app is rendered as we would a normal React app. It also returns an arrow function that calls the ReactDOM.unmount
on the element when it is no longer required.
As a user of the Fronts library, all we need to do inside of the bootstrap.tsx
file is call the boot method, and Fronts will take care of the rest.
site.json
site.json
is another important file that specifies a lot of configuration details for the app. Here’s how it looks for app1
, the container ecommerce app:
{ "name": "app1", "dependencies": { "app2": "http://localhost:3002/remoteEntry.js" }, "shared": { "react": { "singleton": true }, "react-dom": { "singleton": true } } }
Notice how app2
, the products app, is mentioned as one of the dependencies of app1
by passing the localhost deployment URL, followed by remoteEntry.js
as an entry point. When we deploy both apps to production, remoteEntry.js
will be replaced by the production deployment URL.
The shared
object mentions all the dependencies shared by these Fronts applications so that the bundles are not downloaded again on the browser. Specifying the entry point and the shared dependencies in this format makes our bundle function in sync with other Fronts applications, and the library will take care of the rest for us.
Looking at the same file in app2
, we see that it looks slightly different:
{ "name": "app2", "exports": ["./src/bootstrap", "./src/Button"], "dependencies": {}, "shared": { "react": { "singleton": true }, "react-dom": { "singleton": true } } }
Specifically, we see an extra exports
key. This is because app2
exports certain functionality that can be used by other applications. In the example above, the src/bootstrap
file is exported, meaning that the entire app can be imported in other Fronts applications like it was by app1
.
If you take a look at the app1/src/App.tsx
file, you’ll see how the routes are being shared by the two applications:
const routes = [ { path: "/", component: () => <HomePage />, exact: true, }, { path: "/app2", component: () => { const App2 = useApp({ name: "app2", loader: () => import("app2/src/bootstrap"), }); return <App2 />; }, exact: true, }, ];
The route /
is for the homepage, which is handled by the HomePage
component present in app1
:
In the image shown above, the Home button and the Cart button come from the <Navigation />
component present in app1
. The entire pink region is rendered by the <HomePage />
component.
Notice that the route /app2
is being handled by a component that uses the useApp
functionality to generate a component from a separate micro-frontend. If we click on the Browse products button on the previous page, we are taken to the products page, seen below:
The app that displays the three products inside of the light grey background is an entirely different app that is being seamlessly rendered by Fronts.js. We’ll see the proof inside the network tab when we see a call to the bootstrap function of app2
:
You might have noticed that upon clicking on the ADD TO CART button, the number shown at the top of the cart icon increments or decrements accordingly. Given that the two components are theoretically in two different micro-frontends, how are they communicating?
The communication happens courtesy of the globalTransport
functionality exposed by the Fronts library. Inside app1/src/App.tsx
, we’re setting up two global listeners for the increase and the decrease events respectively and modifying the cart count based on it:
const [count, setCount] = useState(0); useEffect( () => globalTransport.listen("increase", () => { setCount(count + 1); }), [count] ); useEffect( () => globalTransport.listen("decrease", () => { setCount(count - 1); }), [count] );
Inside of the app2/src/App.tsx
, we’re emitting the particular events when the corresponding buttons are being clicked:
function addToCart(pid) { const newProd = [...prod]; newProd.forEach(p => { if (p.id === pid) { if (!p.active) { globalTransport.emit("increase"); p.active = true; } else { globalTransport.emit("decrease"); p.active = false; } } }); setProducts(newProd); }
Fronts is ensuring that the events triggered in one of the micro-frontends, the emitter, invoke the corresponding listeners in a different micro-frontend, the listener.
The Fronts library is written on top of webpack’s Module Federation in an attempt to simplify the process of building micro-frontends. Therefore, Fronts includes all the benefits of webpack out of the box. But when we compare both side-by-side, we see that Fronts has the following advantages:
Micro-frontends, which allow developers to work on parts of frontend applications separately and deploy them independently,continue to boom in popularity. Fronts truly shines by allowing developer teams to decouple the lifecycles of separate yet related applications without fearing breakages.
In my opinion, Front’s minimal configuration and its flexibility are currently unmatched. It is definitely worth considering as a framework of choice for your next micro-frontends project. I hope you enjoyed this tutorial.
There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.
LogRocket is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.
LogRocket works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Build confidently — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
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.
Sign up nowIt’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.