Thinking Framework Agnostic — UI Engineering

Laxman Mittal
Walmart Global Tech Blog
9 min readApr 4, 2022

We, as UI Technology evangelists, have been seeing an exponential trajectory in the UI space with the continuous introduction of new frameworks on one hand and the TC39 community constantly looking at how Javascript as a language can be enhanced, and how we can continue to focus on performance in devices with lower memory footprints. The V8 engine for the Chrome browser is constantly released for an enhanced web experience, on the other hand.

With the introduction of new frameworks, what UI specialists continue to think and address, is

Can we do something more generic? Can we start thinking framework agnostic?

Although, when argued, I agree that UI frameworks have made it possible to go mainstream, and the web has now become a dependency on various frameworks like React, Angular, Vue, etc. The front end community at large is continually upgrading to learn the in’s and out’s of various frameworks and gain expertise in specific frameworks.

While there are a few advantages of one framework over the other, there is definitely not one framework out there which could be considered best in all aspects. Therefore, there is no such clear winner, the core reason being agility and business requirements and continuous modules being developed, almost every hour. That is the pace at which the frameworks continue to grow!

We argued that Two-Way data binding in Angular was not very performant, and we also argued that React’s reconciliation algorithm came with some performance costs.

The core idea, is to think about how we continue to support applications developed using various frameworks and host them all using the native browser capabilities.

Wondering what’s so great about that? Applications or utility components that are properly packaged into a web component can be easily and declaratively embedded in your HTML and the web component will take care of instantiating itself. Implementation details of your host application do not matter. What matters is that all the features required to run web components are supported by your targeted browsers. This makes web components framework-agnostic.

If a developer creates a component using Angular or Vue, you can easily use this component in your React application without any special logic. This becomes interesting in the context of micro frontends, where parts of the user interface can be developed by separate teams and even using separate libraries or frameworks, but in the end all parts are brought together smoothly to form one interface.

How can we then go framework agnostic?

Create a shell that is capable of hosting components built by different teams, using different tech stacks and leveraging the DOM API’s. It’s worth saying “micro-frontend is taking the web by storm”.

Do we continue to do the React way, the Angular way, or the Vue JS way? Instead, how about having an architecture that provides a more generic approach and wraps framework features within your component?

Growing code is definitely not about frameworks, it’s about your brain. Your brain thinks in concepts and abstractions. For this reason, we should actually build our apps back-to-front.

What benefits would you see?

  • Reuse: A component is made once and can be reused across different pages, apps, or frameworks.
  • Support: Once fully standardised, it will work on any browser without additional libraries.
  • Maintenance: Since the design is modular, and components are self-contained, they’re easier to maintain.
  • Encapsulation: Each component on the same page can potentially have different styling. We need not worry about clashing element identities. This is due to what’s called Shadow DOM.
  • Reliability: Code is not spread across HTML and JS files, thereby avoiding inconsistencies.
  • Flexibility: Components can be written inline, imported or even compiled.
  • Composability: Components can use or interface with other components.
  • Increasing testability: Since the component design is modular and encapsulated into a unit, testability of your component’s code can increase
  • Mastering specific frameworks: Specific area’s of expertise in different framework is no longer required, thereby saving on onboarding specialised skillset from the job market.
  • Ownership: Component developers would own the architectural piece
  • Support for integration of components, built in various frameworks: The components written in specific frameworks can be incorporated in your application, without having to re-write from the scratch.

On Web Components

If one is not yet familiar with web components, the Mozilla Developer Network article on the topic is probably a good place to start learning about them. In a nutshell, Web Components allow developers to encapsulate HTML, JavaScript and CSS in a way that isolates them from the host HTML document and makes it readily reusable in the DOM via a custom script tag.

Ten Thousand Feet High View of Web Components

Let us take a simple example to understand the underlying concepts in detail where the application loads and embeds three web components with the same content but implemented using Vue, React and plain JavaScript.

Let us break this example into three parts as below:

  1. Defining Web Components
  2. Loading Web Components
  3. Using Web Components

Defining Web Components

To create a web component only few steps are required:

  • Create a custom HTML element.
  • For scoped styles, use the Shadow DOM — This is the driving force behind the encapsulation of web components
  • Register previously created element in the browsers custom elements registry, making it available via a custom HTML tag.

Creating a custom element is as easy as creating a class which extends any already existing HTML elements.

In our basic web component, we have defined two properties:

  • connectedCallback — This function is called when the custom element we are creating here is inserted into the DOM. We use this function to instantiate the actual content of the element, “WELCOME TO WEB COMPONENTS” in this case.
  • disconnectedCallback — This function is called when the custom element is removed from the DOM. We use this function to perform any necessary cleanup actions otherwise we might end up with memory leaks.

Let us make a more generic wrapper for encapsulating our React, Vue or Vanilla JS component, which allows us to do much more.

Let’s dive deep into understanding what is happening in the above lines of code.

The function takes an object with three properties — mount, unmount and styles — and returns a class which extends the browser’s HTMLElement

Remember, we talked about creating a custom web component, merely by extending the HTMLElement class, so in our more generic wrapper we have a function which returns a class extending the HTMLELement.

The mount property of our parameter list defines what should happen when our component is connected to the DOM.

In the connectedCallback function we use this.attachShadow({ mode: “open” }); to attach a Shadow DOM tree to the element to enable scoped styles for this element. This step is important if you do not want to have conflicting style rules between your custom element and the host document, avoiding CSS from bleeding, outside your component. You could alternatively write name-spaced styles that will only be applied in the custom element. Using the Shadow DOM to achieve this makes your element usable anywhere without having to think about conflicting or overwriting CSS rules.

Then we create a template containing the styles inside a style tag and a DOM node which is our mounting point for the content this element is going to render. Finally, the template is cloned and appended to the elements shadowRoot node and the mount function is called with our mounting point element.

Likewise, the disconnectedCallback or the unmount property determines the disconnected state of our component on the DOM. All clean up activities can be performed when the component disconnects/unmount’s from the DOM. This is an important aspect of our wrapper to avoid memory leakage in the browser.

By accepting mount and unmount functions, the above implementation is entirely framework-agnostic and can be used to create custom elements which render anything. That’s all it took for our previous example application to create web components which render React, Vue and plain JavaScript code.

To create a web component which renders a React application, we can use the function defined above as shown below:

In the above snippet, we call createCustomElementWrapper and define the mount and unmount functions to render and clean up the React application. Additionally, as we use Webpack as a bundler, we load our styles into a string using Webpack’s raw-loader and pass it into our function as well. We then register the element with customElement’s registry like below:

Loading Web Components

Web developers can create and use their web components in their application code directly. However, we generally load these generic components either statically or dynamically via scriptTags.

Let us dive one level deeper into getting the web components loaded dynamically on demand in the application. This could be an interesting approach when dealing with lazy loaded modules, specifically when you would like to show components dynamically on user action.

Imagine a situation where multiple teams working on multiple widgets like dashboards, which are required to be loaded on demand, each hosted on different servers.

The below code snippet, could help you load assets/styles and scripts dynamically.

Let’s unravel what is going on in the above code snippet. Since we need to guarantee that we are loading our assets, scripts and style resources only once, we need to do a few checks.

Given that, once we have our user interface composition stitched, we will need to make sure that the interaction on our user interface is happening without colliding events against other views in our layout.

  • Has the script loaded before or is it currently loading? This is done by checking if a script tag with the same source as the script we are trying to load already exists in the DOM. If it does, that means the script is either loading or has already finished loading and has been executed.
  • To find out if the script — if present in the DOM — has finished loading, we set a custom data attribute data-script-loaded on the script element when the script loads. Every time we attempt to load a script, we attach a new event listener to the script tags load event to know when it is done loading. That way, if we try to load the same script twice, the Promises created by calling loadAssets twice resolve simultaneously.

We make use of the loadAssets function in our example application via a React Hook, as per the below code snippet, to make information on loading or error states available to where we embed or use the web component.

The hook above also takes care of re-attempting to load the assets in case of failure.

Using Web Components

Now let us actually use the dynamically loaded web component. For this to work as expected, we assume that for each web component we load into our application, we know the HTML tag and the location of the corresponding assets. This information can be provided by the application serving our custom web component. Here we could leverage service discovery for our backend services.

The following implementation of the example application uses React but can very well be rewritten using Vue or plain JavaScript.

We make use of the useStaticAssets hook from above to embed our ui component into a composition user interface.

There are four things to note here:

  • The component receives the HTML tag, asset paths and additional info as a prop of our custom element, which is framework agnostic.
  • It passes the asset paths to our previously defined hook useStaticAssets.
  • Depending on the loading and error state, it renders different content.
  • It uses the custom HTML tag to render the web component to the DOM.

As described at https://micro-frontends.org you could use skeleton UIs for the loading state of the web components or a spinner, as in the example application. The browser caches the assets of the web components, so loading times are only an issue on first load or after the cache invalidates.

Closing Thoughts

Hope this article has helped in understanding the core functionality of “Thinking Framework Agnostic”. This is definitely not the only way to write your higher order components using a wrapper and going framework agnostic, but this is a good starting point for getting your blocks built one step at a time.

Composition of components built using react, vue & vanilla js into a single interface.

--

--