What Is a UI Component?

A UI Component is a single function that creates elements, sets their content, attaches handlers, and paints the screen -- all driven by one source of truth.

April 20, 20264 min read2 / 3

In the previous post, we built a dataToView function that turned our post variable into visible content every 15 milliseconds. It was a big step forward. One function, one source of truth, no mystery.

But we missed something. We had input and div sitting in our HTML from day one. They were always there, silently assumed to exist. When a user typed a certain name into the input, our handler removed the div by calling jsDiv.remove(). The element vanished, but that vanishing happened inside a handler, not inside dataToView.

We had just broken our only rule.

Elements are view too

Think about what a div appearing or disappearing on the screen is. It is a change the user sees. It is content. And if content must be driven by underlying data, then the very existence of a div or an input needs to be driven by data as well.

We started with the assumption that input and div were permanently there. But "permanently there" is just an implicit piece of state. If we ever want to turn them off based on data, we need to make that state explicit.

The fix is to move element creation out of the HTML and into dataToView. Instead of using document.querySelector to get a reference to an element HTML already created, we use document.createElement to build one from scratch every time.

JavaScript
function dataToView() { // Create from scratch -- conditional on data let jsInput = document.createElement('input'); let jsDiv = post !== 'will' ? document.createElement('div') : ''; // no div if post is 'will' jsInput.value = post; jsDiv.textContent = post; jsInput.oninput = handleInput; // Replace whatever is on the page with what the data says should be there document.body.replaceChildren(jsInput, jsDiv); }

replaceChildren is the key move here. Instead of carefully updating specific parts of the DOM based on what changed, we describe the complete picture from scratch and swap the whole thing in. We do not track what was there before. We just say: "Given my current data, here is the page. Replace everything with this."

This is brute force. But it is also completely predictable.

A UI Component owns everything the user sees ExpandA UI Component owns everything the user sees

The handler only changes data

With dataToView now fully owning the DOM, our handler gets a lot simpler:

JavaScript
function handleInput() { post = jsInput.value; // That's it. We don't touch the DOM here. }

The handler's only job is to change post. Once post changes, dataToView runs again in the next 15ms window and the page updates automatically. The handler never reaches into the DOM. It never decides what to show or not show. It just changes the data and steps back.

One small catch: focus

Because we call replaceChildren and hand the browser a brand new input element every 15 milliseconds, the user's cursor gets kicked out of the input field constantly. We need one extra line:

JavaScript
jsInput.focus();

This puts the cursor back into the input after every replacement, so the user can keep typing without noticing anything happened behind the scenes.

That function is a component

What we have built has a name. A UI Component is a function that:

  1. Creates elements (conditional on data)
  2. Sets their content (conditional on data)
  3. Attaches event handlers
  4. Renders the complete picture to the page via replaceChildren

It fully describes the relationship between data and what the user sees, under every possible state the data could be in. You want to know what the page looks like when post is empty? Read dataToView. You want to know what it looks like when post is "hello"? Read dataToView. The function is the complete truth.

The handler, meanwhile, is just a submission mechanism. The user takes an action, the handler processes that action into a data change, and then dataToView takes it from there.

JavaScript
let post = ''; function dataToView() { let jsInput = document.createElement('input'); let jsDiv = post ? document.createElement('div') : ''; jsInput.value = post; if (jsDiv) jsDiv.textContent = post; jsInput.oninput = handleInput; jsInput.focus(); document.body.replaceChildren(jsInput, jsDiv); } function handleInput() { post = jsInput.value; } setInterval(dataToView, 15);

This is a complete UI component. Around 50 lines of conceptual model, made simple. Every major UI framework -- React, Vue, Angular, Svelte -- is built on this exact idea. The details differ. The principle does not.

The cost of brute force

There is one large problem with this approach. We are rebuilding the entire DOM structure every 15 milliseconds, regardless of whether anything actually changed. If our page has 1,000 elements, we are creating 1,000 DOM nodes, attaching 1,000 handlers, and calling replaceChildren with 1,000 children -- 66 times a second.

That is an enormous amount of work for the browser. Real applications would grind to a halt.

In the next post, we look at how to solve this by keeping a lightweight JavaScript description of what the page should look like, and only pushing the actual differences to the real DOM.


Further Reading and Watching