Automatic Updates with State Hooks

How to eliminate inefficient update intervals by wrapping data changes into hook functions that trigger Virtual DOM generation.

April 20, 20263 min read1 / 2

In our journey building a UI framework from scratch, we've successfully mapped underlying data to a Virtual DOM array, and used that array to render a UI. But there was a glaring flaw in our architecture.

Because our UI is purely driven by data, and we don't know when that data might change across an entire application, we resorted to the most brutal solution possible: running a setInterval to recreate the entire DOM every 15 milliseconds.

JavaScript
// Terribly inefficient setInterval(updateDOM, 15);

We need to stop running our update loop constantly. We only want updateDOM to run when our data actually changes.

The Problem with Manual Updates

The easiest way to fix this is to just call updateDOM() inside our event handlers manually:

JavaScript
function handleInput(e) { // Update the data name = e.target.value; // Then manually trigger the visuals updateDOM(); }

While this works, it violates the core principle of our new declarative paradigm. We never want developers to have to manually remember to sync the view. If a developer forgets to call updateDOM(); inside a handler, the data will update but the screen won't. We are back to the bugs of imperative programming.

We need a way to wrap the act of updating data and the act of triggering updateDOM into a single, indivisible task.

Abstracting the Update into a Hook

How do we wrap two tasks up? With a function.

Instead of writing name = e.target.value, let's restrict our developers. They are no longer allowed to change data directly. Instead, they must pass their changes to a utility function we provide, often called updateData or a "setter".

JavaScript
function updateData(key, value) { // 1. Update the actual data store data[key] = value; // 2. Secretly trigger the DOM update updateDOM(); }

Now look at how our handler changes:

JavaScript
function handleInput(e) { // The developer just thinks about data updateData('name', e.target.value); }

The developer's experience is perfect: they respond to a user event by updating the data. They literally do not have to think about the DOM or rendering.

But behind the scenes, our updateData function catches that intent, alters the data object, and immediately triggers createVDOM() and replaceChildren(). The view updates dynamically exactly--and only--when the data changes.

A state hook function wrapping data assignment and updateDOM in one step ExpandA state hook function wrapping data assignment and updateDOM in one step

The Birth of useState

If you use React, this pattern is the exact origin of the useState hook.

When you call const [name, setName] = useState(''), React gives you the current data value (name), and a setter function (setName). It strictly forbids you from doing name = "FM". You must go through the setter: setName("FM").

Why? Because setName is doing exactly what our updateData function did. It updates the internal data variable, and then quietly triggers React to execute your component function again (our createVDOM) to figure out what the new UI should look like.

And this reveals why they call it a "hook". It allows a developer's component to hook into the framework's overarching loop of state persistence and rendering. You use the hook to say "change this data", and the framework says "I'll handle the data, and I'll handle updating the screen."

But there is still one remaining performance nightmare. Even though we are only running updateDOM when the data changes, the updateDOM function itself is currently destroying every single DOM element on the page and recreating them from scratch via replaceChildren(). If you have a table with 5,000 rows, typing a single letter in a search bar destroys and recreates 5,000 table rows.

We need a diffing algorithm.


Further Reading and Watching