Bulletproof web component loading



This content originally appeared on Go Make Things and was authored by Go Make Things

Today, I wanted to talk about the “right” way to load web components to ensure they work predictably every time. Let’s dig in!

The challenge

Web components are self instantiating.

Let’s imagine you’re creating a table of contents. When you use a traditional JavaScript library, you explicitly instantiate the library on a specific element.

<div id="toc"></div>
const myTOC = new TableOfContents('#toc');

But with web components, all you need to do is include the custom element associated with the component.

<table-of-contents></table-of-contents>

The JavaScript automatically runs on that element once it…

  1. Is created in the DOM, or
  2. The JavaScript for the web component is loaded

… whichever happens last.

This can create problems

If you load your JavaScript in the footer, or use defer or the [type="module"] attribute, everything is fine.

But if you load the script for a web component in the <head> as a traditional <script>, you can run into problems.

Consider the <table-of-contents> component. The JS for it looks for headings on the page, and generates a list (<ul> or <ol>) or links to those headings.

What happens if you load your table-of-contents.js file in the <head>?

The second the browser hits that <table-of-contents> component, the JavaScript for it executes. It does this before it’s even loaded the child elements inside the component.

If you put your <table-of-contents> at the top of the document (and that’s the natural place for, given what it does), that probably means there aren’t any headers on the page yet to find.

The script breaks.

The simple fix

The obvious fix is load your JS in the footer, defer it, or load it asynchronously as an ES module.

But as my friend Alex Riviere pointed out, people can’t always control when and how scripts load.

Maybe it’s included as a third-party script. Maybe their CMS always loads code in the <head>. Maybe they just don’t know how exactly to do it properly.

Whatever the reason, if you’re building something that’s going to be used in unpredictable ways, you owe it to the end user to make it as stable and resilient as possible.

The web is a weird and unpredictable place, and beginners and non-technical folks deserve good software just as much as seasoned devs do!

Bulletproofing web component loading

The trick, then, is to wait for the DOM to be ready. Remember that from the jQuery era? It’s back, y’all!

I’m using a pattern in Kelp to make this predictable across all of my web components.

Every component has an .init() method where I run all of my startup code for the component.

Inside the connectedCallback() method, I check if the DOM is ready. If it is, I run this.init() immediately. If not, I wait for the DOMContentLoaded event, and run this.init() then.

// Initialize on connect
connectedCallback () {
	if (document.readyState !== 'loading') {
		this.init();
		return;
	}
	document.addEventListener('DOMContentLoaded', () => this.init());
}

I’m grateful to Patrick Nelson for catching this issue in Kelp and rubber ducking the best path forward with me.

Patrick also made a few improvements to the Kelp testing process.

You can learn more about Kelp over at KelpUI.com.

Like this? A Lean Web Club membership is the best way to support my work and help me create more free content.


This content originally appeared on Go Make Things and was authored by Go Make Things