Choosing a tech stack for a new project is always full of compromises. There are many good frameworks and libraries, but finding the one that perfectly fits your requirements is a real challenge. Building everything from scratch can be a decent choice, but it will only work with unlimited time and resources. Therefore, you always have to choose from the available options.
Choosing The Perfect Tech Stack
Every framework and library is a tool initially designed for solving a specific type of task. The problem is that a complex software system has tons of different tasks you must solve to satisfy the client’s business needs. Here, the team has to choose a development tool that solves the maximum number of available technical tasks and ensure that, at the same time, the cost of solving the remaining tasks will be acceptable. Otherwise, they can be excluded from the client’s list of business requirements. This way, developers remain flexible and maintain a balance between feature richness and development cost.
Sometimes, it’s challenging to find the perfect balance point due to various technical reasons and limitations, including:
- Performance. There’s a web table that works with an unlimited number of rows and columns or a scheduler that processes a massive amount of recurring tasks;
- Responsive UI. The adaptability of the UI for different screens;
- Customizability. Ease of customization of ready-to-use components from the library package.
The difficulty of balancing can have different consequences. You’ll face either more expensive development or fewer product features leading to a drop in customer profits.
At XB Software, we actively use the Webix and DHTMLX libraries for building high-performance web applications for business. Also, we have deep expertise in using React to create tons of entertainment applications.
Using these tools, we often face the necessity to combine the features from both these worlds. We’ve tried different approaches and have opted for micro frontends. It allows us to leverage the features of Webix, DHTMLX, and React used on the same page. We managed to work around the limitations of each library and take full advantage of their benefits.
Here, we’ll show how to combine Webix and React to build top-notch web applications. However, the described approach will work well with combining other technologies. That’s the power of micro frontends.
How to Implement Micro Frontends
In micro frontends, there are the following essential terms:
- shell is where pieces of applications or the whole application will be embedded;
- remote is a part of the app or the whole application to be embedded into the shell.
You can build shell and remote as absolutely any other application implemented on any framework or library. The overall experience, however, shows that React (or native WebComponents as an alternative) is better suited for building shells if remotes are made with different technologies. The reason is that React allows you to wrap anything in a reusable component conveniently. If all remotes use the same technology, it also makes sense to use it for shells. For example, if you implement all remotes with Angular, it’s also better to use Angular for shells.
Read Also Micro-frontends: The Future Sometimes Comes in Miniature Sizes
To implement a micro frontend, you should analyze what frameworks will be used and what exactly will be imported into the shell. Following these factors, you can choose the specific technology to build the micro frontend. Here are the available options:
- bit (Home Page, GitHub) is a framework that is better suited for importing components (buttons, inputs, tables, etc.) from remote to shell and subsequently building with them a monolith page;
- single-spa (Home Page, GitHub) is great for importing individual components and whole applications. Note that in this case, the remote to be imported must implement life cycle functions (boilerplate, mount, unmount) that single-spa uses. In the case of popular frameworks, such as React, Angular, Vue.js, or Svelte, libraries for life cycle functions are already written by their developers;
- webpack module federation (Home Page) allows adding an extra plugin in the webpack config. For the remote, this plugin will export components or whole apps. For the shell, it’ll import the remote’s components or entire apps into it. Due to the significant number of configuration options that webpack provides, setting up the config in this case can be difficult;
- vite-plugin-federation (GitHub) implements the same approach as webpack but is much easier to config and work with.
Let’s consider an example of building a shell (pure JavaScript) and remote (React, Webix) with vite-plugin-federation.
Implementing the remote with React:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' import './index.css' function mount(el) { ReactDOM.createRoot(el).render( <React.StrictMode> <App /> </React.StrictMode>, ) } export { mount } |
And here’s the vite config:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import federation from '@originjs/vite-plugin-federation' export default defineConfig({ server: { port: 4001 }, plugins: [ react(), federation({ name: 'remotereact', filename: 'remoteEntry.js', // Modules to expose exposes: { './App': './src/bootstrap.jsx' }, shared: ['react', 'react-dom'] }) ], }) |
Implementing the remote with Webix:
1 2 3 4 5 6 7 8 9 |
import MyApp from 'myapp'; function mount(el) { webix.ready(() => new MyApp({}).render(el)); } export { mount }; |
Vite config:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import federation from '@originjs/vite-plugin-federation'; import {defineConfig} from 'vite'; import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ server: { port: 4002 }, assetsInclude: ['./assets'], plugins: [ tsconfigPaths(), federation({ name: 'remotewebix', filename: 'remoteEntry.js', // Modules to expose exposes: { './App': './sources/bootstrap.ts' } }) ] }); |
Implementing the shell with pure JavaScript:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import { defineConfig } from 'vite' import federation from '@originjs/vite-plugin-federation' export default defineConfig({ server: { port: 4000 }, input: './index.js', plugins: [ federation({ name: 'main', remotes: { remotereact: "http://localhost:4001/assets/remoteEntry.js", remotewebix: "http://localhost:4002/assets/remoteEntry.js" } }) ] }) |
Using remote apps in shell:
1 2 3 4 5 6 7 8 9 10 11 |
function importAndMount(path, elId) { const lazyLoadApp = () => import(path); lazyLoadApp().then((imp) => { const { mount } = imp.default; const appRoot = document.getElementById(elId); mount(appRoot); }); } importAndMount('remotereact/App', 'react-root'); importAndMount('remotewebix/App', 'webix-root'); |
Here are some tricky parts you should keep in mind:
- Since the shell and remote are hosted on different ports, if you have a relative path for static files in your app, they will not load because the browser will try to request them from the shell’s URL, not the remote’s URL. To solve this, you should use the path, taking into account the remote’s URL;
- The styles of all remotes will be on the same page, which means there is a risk of overlapping the styles of one application with another. ShadowDOM can solve this problem. However, such an approach does not apply to some libraries and frameworks;
- Global variables that are set in the window can also overwrite each other. Make sure you keep that in mind.
When using module federation (webpack or vite) you should always remember the shared field in the builder config. It allows sharing libraries between shells and remotes. For example, imagine you have the following configs:
- shell: shared: [‘react’, ‘react-dom’, ‘webix’]
- remote(react1): shared: [‘react’, ‘react-dom’]
- remote(react2): shared: [‘react’, ‘react-dom’]
- remote(webix1): shared: [‘webix’]
- remote(webix2): shared: [‘webix’]
Here, library sharing allows remotes not to load libraries again but take those already passed to them. It reduces the weight of the application build and allows you to control the versions of the libraries they use. You can learn more about this here (webpack) and here (vite).
Another important aspect of micro frontend development is event bus implementation. Event bus may represent an implementation of the PubSub concept. Here are some implementation options:
- Global native events that are attached to a hidden element. There’s a significant disadvantage in this case. Micro frontends implemented using iframe won’t support them:
1234567891011121314// Creating a comment elementconst elem = document.createComment("Event Bus");document.body.appendChild(elem);// Subscribe to messageselem.addEventListener("channel-1", (event) => {console.log(event.detail);});// Publish messagesconst event = new CustomEvent("channel-1", {detail: { message: "Hello World" },});elem.dispatchEvent(event); - You can manually write a class that will implement this concept with methods publish, subscribe, unsubscribe. The weak side is the need to pass an object of this class to all remotes;
- Broadcast Channel (Documentation Page) is the ultimate solution for all cases. It enables communication between frames, iframes, tabs, and windows if they all have the same origin. Plus, it’s supported by all browsers, including mobile ones. Here’s an example:
1234567891011121314// Connecting to a channel:const BC = new BroadcastChannel("My Channel");// Publishing a messageBC.postMessage({ data: { foo: "bar" } });BC.postMessage("Test");// Subscribing to messagesBC.onmessage = (event) => {console.log(event);};// DisconnectingBC.close();
Conclusions
Everything has pros and cons, and micro frontends are no exception. The tools you use to build them (e.g., bit, single-spa, and others) complicate the source code of a web application which contradicts the natural intention of all developers to keep it as simple and intuitive as possible.
A micro-fronted application is slightly more difficult and expensive to develop than an application with only one library or framework. If you think of using a micro frontend for implementing a minor feature of your product, our advice is not to spend your time and abandon the feature. If there’s a major feature that you plan to build using micro frontends, we hope you find this article useful.
Contact us if you’re looking for a developer to build a solid solution addressing your business needs.