A Cure for the React Disease

cd ~/blog

For the quick-witted eye, you may have noticed that one of the tags in this article (the first one, if you didn’t notice) changed the number from zero to the total number of words in this article.

Cool, right?

Now let me tell you, this is a static page, rendered based on a markdown text, with no ReAcT mAgIC in it. But how can I make a dynamic, changing value appear all of a sudden on this page without the rEcAt?

For some, this would be absurd—impossible.

The React Disease

I’ve spoken with web engineers who have no idea how to write a page without React, or worse, without NextJS. For them, a page can only be created with these tools.

The more I talked, the more I saw how little knowledge these people actually had. And to be honest, with the number of Theos on the internet screaming out loud about modern web development, I shouldn’t blame them for not seeing things differently.

Still, I blame both the engineer for the lack of knowledge and the Theos for their lack of awareness. After all, for the Theos, all they see is advertising for NextJS, Vercel, Hostinger, and so on.

And to make matters worse, people forget that, for the majority of websites, they can just be simple static HTML pages. The blog you’re reading right now is a static page, served with a few CDNs distributing it across the globe, some cache headers so static assets aren’t reloaded unnecessarily, and voilà, you’ve got a fast, functional website (most of the time).

What Can We Do?

Open standards, this is the only metric that will last. All of these frameworks solve specific problems: React solves Facebook’s problem, Angular solves Google’s problem, Vue solves the community’s problem, and so on.

So what solves the browser’s problem? Web Components.

I have to admit, when Web Components first appeared, let’s say 2020 for simplicity, even though they stabilized around 2018, they were a mess. The APIs didn’t fully solve the issues, the React disease was already widespread, and the solution wasn’t fully there.

But today, a few years later, most of the critiques I had have been resolved. More importantly, my view of the web has changed. I’ve been cured of the React disease. And what a difference that makes.

The Island Architecture

The island architecture is a simple pattern, really. You don’t need the entire page to be rendered on the client. From my experience over the years, I’ve realized that 90% of projects could be server-rendered. Most of the pages you interact with daily could be mostly static, no interaction needed. Add some CDNs and a few strategically located servers (and I’m not even talking Google or Amazon level infrastructure here), and…

Your app would fly.

But that remaining 10%, that’s where the magic happens. You don’t need the whole page to be a React app. You could, in theory, just plug React into that one part and make it dynamic, a little island of interactivity on a mostly static page.

With that mindset, you don’t even need React. Why ship the whole framework when the browser already supports Web Components natively, with no special downloads or “magic glue” needed to bridge the DOM and the virtual DOM? No “Fiber” to figure out what changed and when.

A Few Lines of Code Worth Thousands of Words

Want to see how it works? Here’s the full TypeScript for the component:

export class CountFrom extends HTMLElement {
	private el!: HTMLElement;
	private words: number = 0;
	private duration: number = 2000;
	private startTime: number = 0;

	constructor() {
		super();
		this.defineEl();
		this.countWords();
		this.animateCount();
		this.updateText();
	}

	updateText(num: number = 0) {
		this.innerHTML = `Word Count <pre>${num.toFixed(0).padStart(this.words.toFixed(0).length, " ")}<pre>`;
	}

	defineEl() {
		const el = document.querySelector(this.getAttribute("target") ?? "");
		if (!el) throw new Error("Failed to locate element.");
		if (!(el instanceof HTMLElement))
			throw new Error("Element isn’t an HTMLElement.");
		this.el = el;
	}

	countWords() {
		const text = this.el.innerText.trim();
		const count = text === "" ? 0 : text.split(/\s+/).length;
		this.words = count;
	}

	animateCount() {
		setTimeout(() => {
			this.startTime = performance.now();
			requestAnimationFrame(() => this.updateCount());
		}, 1000);
	}

	updateCount() {
		const progress = Math.min(
			(performance.now() - this.startTime) / this.duration,
			1,
		);
		const cubicProgress = Math.pow(progress, 3);
		const currentCount = Math.floor(this.words * cubicProgress);
		this.updateText(currentCount);

		if (progress < 1) {
			requestAnimationFrame(() => this.updateCount());
		}
	}
}

A lot of code, right? But it’s not really that much, mostly it just looks long because I’m not a great developer, and I like to make things complex without reason. But we can go over how it works.

If I didn’t want the animation, the component could be simplified to this:

export class CountFrom extends HTMLElement {
	constructor() {
		super();
		const el = document.querySelector(this.getAttribute("target"));
		const text = el.innerText.trim();
		const count = text === "" ? 0 : text.split(/\s+/).length;
		this.innerHTML = `Word Count <pre>${count.toFixed(0)}<pre>`;
	}
}

customElements.define("count-from", CountFrom);

Much shorter, right? To use it, you just do this:

<count-from target="some.css.selector"></count-from>

Very simple, and I’m not even using the <template> tag here. This is as basic as it gets. And with this, you can do a lot. Want a line-by-line explanation? Sheesh… okay.

// This is the name of the class; it should be unique
export class CountFrom extends HTMLElement {
	// All web components extend a built-in element—in this case, HTMLElement
	constructor() {
		// This runs when the component is added to the DOM
		super(); // Calls the HTMLElement constructor
		const el = document.querySelector(this.getAttribute("target")); // Grabs the element targeted by the component's "target" attribute
		const text = el.innerText.trim(); // Gets and trims the inner text
		const count = text === "" ? 0 : text.split(/\s+/).length; // Uses a RegEx to count the number of words
		this.innerHTML = `Word Count <pre>${count.toFixed(0)}<pre>`; // Sets the component's inner HTML to show the word count
	}
}

customElements.define("count-from", CountFrom); // Registers the <count-from> tag using the browser’s Web Components API

See? It’s not that hard.

Now I ask you: do you really need React? Very often, the answer is no. Like, 90% of the time it’s no. The remaining 10%? That’s another story. But I’m pretty sure you’re not in that 10%, after all if you were on the 10% this blog post is already useless to you.

Some extra reading if you want:

(()=>{})()