Introduction
React has dominated front-end development for over a decade. It's powerful, widely adopted, and backed by Meta. But it also ships hundreds of kilobytes of runtime code, demands build tooling, and introduces a mental model that can feel like a black box. What if you could build a reactive UI library with just the browser's native APIs? A recent deep-dive on Pedro's blog challenges the assumption that React is mandatory for building modern interfaces. By exploring a minimal UI library in plain JavaScript, the article shows how you can achieve reactivity, componentization, and state management without a single dependency.
This isn't about abandoning frameworks entirely—it's about understanding what's under the hood. When you strip away the layers of JSX, virtual DOM, and reconcilers, you're left with a few core concepts: state, rendering, and event handling. All of these are natively supported by JavaScript in 2026. In this guide, we'll walk through the steps outlined in the source article, building a tiny but functional reactive UI library from scratch. You'll learn how to harness Proxy, custom elements, and event listeners to create a library that's small enough to fit in a tweet and fast enough for production use cases.
Why Vanilla JavaScript Makes Sense (Again)
The JavaScript ecosystem has swinging pendulums. In the early 2000s, raw DOM manipulation was the norm. Then came jQuery, then Angular, then React. Each framework solved real problems—especially cross-browser consistency and complex state management. But as frameworks grew, so did their runtime cost. React's core is around 100 KB minified (without ReactDOM). Vue is lighter, but still substantial. Preact, a React alternative, is about 3 KB, but it's still a framework.
The article argues that for many projects—landing pages, interactive widgets, documentation portals—the overhead is unjustified. Modern browsers provide robust APIs: document.createElement, CustomEvent, Proxy, Reflect, MutationObserver, and native Web Components. Together, these can replicate the essential features of React in 50 lines of code.
Indeed, you can write components as functions, manage state via reactive objects, and re-render only when dependencies change. The result is faster initial loads, no build step required (you can use ES modules directly), and a deeper understanding of how browsers work.
Core Concepts: Reactivity and Rendering
Before diving into code, let's clarify two terms:
- Reactivity: The ability of a UI to automatically update when state changes. In React, this is achieved via the virtual DOM and reconciliation. In Vanilla JS, we can use
Proxyto intercept property writes and trigger updates. - Rendering: The process of turning state into DOM. React uses JSX and
React.createElement. We'll build a similarhfunction that creates DOM elements from a tag name and props.
The core idea from the source article is to keep a single state object (or multiple) and use a subscriber pattern to re-render the affected parts of the DOM when state changes. No diffing needed—just replace the node's content or update specific attributes.
Step 1: Setting Up a Reactive State
We'll start with a function reactive that wraps an object in a Proxy. This proxy will forward gets and sets, and notify listeners via a simple event emitter. In the article, the author describes a minimal implementation with a onChange callback.
const state = reactive({
count: 0
});
function reactive(obj) {
const listeners = new Set();
return new Proxy(obj, {
get(target, prop) {
return Reflect.get(target, prop);
},
set(target, prop, value) {
const result = Reflect.set(target, prop, value);
listeners.forEach(listener => listener(prop, value));
return result;
},
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
});
}
Note: The subscribe method is not a standard Proxy trap; we're attaching it to the proxy object. This is valid because Proxy can return access to target methods, but here we're mixing concerns. A cleaner approach, as in the source article, might use a separate on function. But this gives the idea.
Now, every time state.count = 1 is executed, all subscriber callbacks run. The browser automatically re-renders the elements that depend on count.
Step 2: Building a createElement Helper
React uses React.createElement(type, props, children). We'll build a similar function h that returns actual DOM nodes, not virtual nodes. This avoids the virtual DOM entirely. The source article emphasizes that the DOM itself is the source of truth.
function h(tag, props, ...children) {
const element = document.createElement(tag);
if (props) {
for (const [key, value] of Object.entries(props)) {
if (key.startsWith('on') && typeof value === 'function') {
const eventName = key.slice(2).toLowerCase();
element.addEventListener(eventName, value);
} else if (key === 'style' && typeof value === 'object') {
Object.assign(element.style, value);
} else if (key === 'class') {
element.className = value;
} else {
element.setAttribute(key, value);
}
}
}
for (const child of children.flat()) {
if (child instanceof Node) {
element.appendChild(child);
} else {
element.appendChild(document.createTextNode(String(child)));
}
}
return element;
}
Now we can write h('button', { onclick: () => state.count++ }, 'Click me') and get a real <button> element.
Step 3: Component Abstraction
In React, a component is a function that returns JSX. In our mini-library, a component is a function that takes props and state and returns a DOM node. For example:
function Counter() {
const wrapper = h('div', { class: 'counter' });
const display = h('span', { id: 'count-display' }, state.count);
const button = h('button', { onclick: () => state.count++ }, 'Increment');
wrapper.append(display, button);
// Subscribe to state changes and update display
state.subscribe((prop) => {
if (prop === 'count') {
display.textContent = state.count;
}
});
return wrapper;
}
This component builds its DOM once and updates it reactively. The subscription is set up inside the component, so when state.count changes, only the display text is updated. This is more efficient than re-rendering the whole component.
The source article points out that this approach mimics the
Comments