Last updated on

You don't need React (until you do)


Pedro Thomas recently wrote a post rebuilding the React tutorial with about 50 lines of vanilla JavaScript: a chainable DomBuilder wrapper over document.createElement, plus a useState that's nothing more than publish/subscribe. He walks it through the same examples React's own docs use: nested components, a profile list with a loading state, a task list, tic-tac-toe, tic-tac-toe with undo. Each one works. The post is honestly scoped, too. Pedro says outright he wouldn't reach for this library on a big, complex app. He isn't arguing that React is bad.

But the title does real work. Read as pedagogy, "you don't need React" is fine advice: build the thing to understand the thing. Read as career guidance for someone deciding where to spend their learning hours, it smuggles in a claim that doesn't hold up against React's own history, that frameworks are fashion sitting on top of timeless fundamentals, so the durable move is to learn the fundamentals and treat the framework as optional scaffolding. That's the claim worth pushing back on.

What the toy gets right, and where it leaks

Read side by side, the two versions look closer than you'd expect. A component is a function that returns a tree, in both. Composing components, a button inside an app, cards inside a list, reads almost identically once you get past JSX. That's the honest part of Pedro's pitch: React's core idea is small enough to rebuild in an afternoon, and doing so is a legitimate way to make the substrate legible. createElement, event listeners, a subscription stop being magic words.

Where it comes apart is the task list. Because the DomBuilder version re-renders by tearing down the whole subtree and rebuilding it (removeChildren().append(...) on every state change), a genuinely controlled text input would lose focus and cursor position on every keystroke. So Pedro doesn't control it. He leaves the input uncontrolled and reads input.element.value directly at submit time, and for the checkbox he reaches past the abstraction entirely to poke cb.element.checked = task.completed.

That's not a nitpick. It's the whole story in miniature. React's reconciler keeps the same DOM node across renders when a component's identity is stable, which is what lets a fully controlled value plus onChange input keep focus through every keystroke. The DomBuilder version can't do that because it never diffs, it only knows how to erase and redraw. So the moment the toy needs the one thing production UI needs constantly, an input a user is actively typing into, it has to drop back to hand-managed DOM state: the very thing it set out to replace.

Feeling that failure yourself is useful. It's probably why the controlled/uncontrolled distinction, and the reconciliation model under it, will never confuse you again (a fair number of working React developers can't fully explain the bug you'd just have avoided). The lesson lands. It's the framing around the lesson, "so maybe you don't need the thing that solves this," that doesn't.

The axis underneath: imperative vs. declarative

The more interesting split isn't vanilla versus React. It's imperative versus declarative, and the toy sits closer to the imperative side than its "UI = f(state)" framing suggests.

Imperative code spells out the steps: create this node, append it there, wipe those children, set .checked directly. You own how the UI moves from one state to the next. Declarative code describes the destination instead: for this state, the UI looks like this, and the runtime works out the steps to get there.

The DomBuilder library looks declarative because components are functions returning trees. But its update mechanism is imperative. You wire onChange(render) by hand, and render() tears the container down and rebuilds it from scratch. The .checked poke and the uncontrolled input are imperative leaks showing through a declarative-looking surface. React is declarative by default (you return the tree you want and never touch a node directly) and drops into imperative mode only through marked escape hatches like refs. Which one is the default and which is the deliberate exception is the real difference between the two approaches, more than line count or bundle size.

This isn't an accident of taste. UI frameworks went declarative across the 2010s and didn't come back: React, Vue, Svelte, SwiftUI, Jetpack Compose, Flutter, and HTML, CSS, and SQL before any of them existed. Describing target state scales in humans, and human maintainability, not runtime overhead, is the dominant cost of real software. Imperative code still wins at the edges: game loops, canvas and WebGL, huge lists, animation, anywhere the diffing itself is overhead you can't afford, or the whole surface is genuinely a single trivial widget, which is Pedro's actual, narrower point. The mature position is declarative by default and imperative on purpose at the boundaries. The toy inverts that ordering for anything past the smallest case.

The pattern treadmill isn't proof of fashion

Here's the part of "you don't need the framework" that has real bite: almost every "best practice" from ten years ago now looks like ceremony. jQuery's manual DOM wiring gave way to components. Two-way binding and Angular 1's digest cycle gave way to unidirectional data flow. Class components and the HOC/render-prop wrapper stack gave way to hooks. Redux's actions-constants-reducers-thunks boilerplate gave way to Redux Toolkit, then to Zustand and signal-based stores. That trajectory looks a lot like fashion, and "fashion" is exactly the word that makes rebuilding your own tools sound like the sturdier bet.

But look at where the walkbacks actually landed. CSS-in-JS got abandoned. Its runtime cost of computing and injecting styles on the client showed up on real devices, and it's fundamentally incompatible with server components, which don't run on the client to inject anything. It didn't land back at global stylesheets and specificity wars. It landed at zero-runtime scoped styling: Tailwind, CSS Modules, Vanilla Extract, plus native CSS finally catching up with nesting and layers. Webpack and Create React App got walked back for config sprawl and slow builds. They didn't land back at Grunt task chains. They landed at Vite and Rust-based tooling doing the same job an order of magnitude faster.

Every one of those walkbacks kept the win (scoping, colocation, fast feedback) and shed only the part that turned out to be overreach. That's a ratchet, not a pendulum: the field drops what didn't pay for itself and keeps the ground it already won. None of these corrections ever landed back at hand-wired imperative DOM manipulation, which is the tell that "it's all just churn" doesn't hold up. What looks like fashion from a distance is, close up, mostly self-correction once the true costs of an approach become visible. That's a different thing, and it takes exactly the kind of judgment that "just learn the fundamentals and skip the framework" can't supply on its own, because the fundamentals keep moving too.

The verdict

It's tempting to call all this a draw: the toy for people optimizing for a résumé line, the deep dive for people who want to actually understand what they're using. Resist that. It only holds together by imagining the framework-invested engineer as someone who memorized 2013's API and stopped there.

React is about thirteen years old, and it hasn't stayed still under the people who stuck with it. Classes gave way to hooks, object-oriented to functional, inside the same library. Client-side rendering grew server components. The boundary lines between what belongs to HTML, CSS, and JS got redrawn more than once. None of that is what "staying invested" looks like if you're picturing someone frozen in place. It's what staying invested actually looks like: updating your fundamentals in place, inside the tool, as the tool itself absorbs the industry's paradigm shifts. In a living ecosystem, the framework isn't a fashion layer sitting on top of timeless fundamentals underneath it. It's where the fundamentals get worked out and shipped.

Which reframes what the exercise is actually good for. Rebuilding a UI library from scratch isn't a competing strategy for staying valuable over a career. It's a supplement: an efficient, enjoyable way to make the substrate legible and come back a sharper user of the real thing. Do it for curiosity. Do it because reading an entire runtime in one sitting feels good. Just don't do it on the belief that the deep-in-the-framework path is the shallow one, because the record doesn't support that, and every year React keeps absorbing changes that would have broken a purely hand-rolled foundation makes it harder to believe.

One last thing worth sitting with. Part of what a from-scratch rebuild like this teaches, a UI as a whole tree of state re-rendered wholesale on every change, is itself a paradigm that fine-grained reactivity and server components have already started moving past inside React's own ecosystem. Even the fundamentals aren't frozen. That's not an argument against learning them. It's the argument for learning them where they keep getting updated, instead of in a fifty-line snapshot of what they used to be.

Comments

No comments yet. Be the first to comment.

Leave a comment