This post is part history lesson, part speculation—why web programming is hard, why it’s hard to fix, and how React might help. It’s heavy on context and light on code, and if you’re looking to better understand its technical underpinnings, Dan Abramov's deep dive, “React as a UI Runtime” is well worth your time.

Pete Hunt almost looks sheepish. “We announced this crazy new JavaScript library, and we were lampooned by everybody. Everyone was making fun of us. We had these weird angle-brackets in your JavaScript, and we just didn’t do a great job of messaging it.”

That was 2014. The rest of us were a year or so into our journey with React.js: far enough past the angle-brackets to appreciate how simple building user interfaces could be. Forget the quirks of the Document Object Model (DOM) or synchronizing models with rendered state. We could just declare interfaces as trees of minimally-dependent components and React would take care of the rest. Our MVC applications had found a new “V”, and oh! What a delightful “V” it was.

We didn’t know then where React was headed. We didn’t anticipate the ecosystem of tools that would spring up around it, or its usefulness for building interfaces outside the browser (though we’ll stick to the browser’s story here). We recognized a new sort of thinking that solved many old problems. We didn’t have a clue how deep it would go.

We’ll get to that in a moment. But first we need some time in the past.

Picture yourself in a simpler time. Tim Berners-Lee had envisioned the web as an information system, a vast accumulation of documents connected by standard protocols, and in 1993 that’s exactly what it was. Non-technical audiences were just beginning to access the web through a new browser, Mosaic, whose development–like many projects of the early internet–was supported by public money. It would be two more years before Mosaic was licensed by Microsoft as the core of Internet Explorer 1. Netscape Navigator (which would turn Mosaic’s lead developer, Marc Andreesen, into a household name) wasn’t so much as a gleam in anyone’s eye.

Then, the browser wars. We remember the struggle between Microsoft and Netscape for its decisive legal battle, but the skirmishes that led to Netscape’s demise and United States v. Microsoft were fought with technology.

Both vendors piled on proprietary features to try and gain the upper hand in an innovative frenzy that gave shape to the modern web. Netscape released JavaScript and a rudimentary DOM for it to interact with (1995); Microsoft countered with the first implementation of CSS (1996).

CSS was an oddity in that its specification preceded any implementation. More often standards responded to the features they were ostensibly standardizing, as with ECMA-262 (1997), which described the implementations of JavaScript and Microsoft’s reverse-engineered JScript, and the “Intermediate” DOM (1998). By the time a standard was being debated the chicken had long since flown its proverbial coop. Compatibility for websites already using vendor-specific implementations meant back-compatibility, broken functionality, or both.

The result is a modern web built more from necessity than intent. With the benefit of hindsight we can easily identify both well-adapted features and ones that are awkward, weird, or downright hairy–but that experience was not available at the time. It was “build the thing or die trying”, and here we are.

Without web forms to validate, cursors to trail or alert s to pop up, JavaScript is both an unremarkable scripting language and the only show in town.

Lucky for us, useful APIs have never been far away. During the first browser war, both dozens of proprietary APIs were engineering, shipped, and adjusted by both Microsoft and Netscape (which the other would promptly reverse-engineer or ignore). Consuming a new API usually meant checking a script’s host environment for support, normalizing “known” gaps between different browsers and browser versions, and providing fallbacks when clients lacked support.

It wasn’t web development’s finest hour.

But as standards bodies deliberated and browser vendors traded horses, relief came from within. Rather than waiting for browser standards to converge, a new generation of JavaScript libraries took it upon themselves to wrap standard APIs over the quirks of different browsers. Developers using tools like Prototype or jQuery could call browser functions with reasonable confidence that vendor-specific quirks would be handled in a reasonably graceful way. For a few extra kilobytes of library and a modest performance hit, developers could ‘write once, run anywhere’ in what’s become a familiar pattern–JavaScript solving JavaScript with–what else?–more JavaScript.

Somewhere in the darkness, web developers realized they didn’t need to wait for standards or browsers to begin improving their lot. Old patterns and new bugs could be “fixed” from the familiar confines of the JavaScript runtime. If an idea caught on, it could always evolve into an appropriate specification. And no-one would mourn its passing if it didn’t. Back-compatibility doesn’t mean much in a sandbox.

But what if instead of normalizing imperfect APIs we could replace them with something better? What if–what if–we replaced the imperative, mutable, and somewhat vendor-specific DOM with a declarative, immutable, normal simulacrum that we could blow away and recreate at virtually no cost? Just like jQuery seven years before, our new DOM could hide the quirks of different browsers (or even different platforms). But unlike jQuery, this new DOM could be faster, more predictable, and easier to manipulate and extend.

You see where this is going.

React was more than the funny little angle brackets in JSX. It also brought the radical idea that programmers don’t need to deal with the DOM. Just pass a well-formed React component tree to ReactDOM.render and voilà! the renderer will take care of the rest. Where JavaScript is a clapboard addition to a basically-static document; React is a dynamic, interactive document.

React embraces the fact that rendering logic is inherently coupled with other UI logic: how events are handled, how the state changes over time, and how the data is prepared for display. – React Docs

That realization changed how we think. We worried less about performance optimizations–that’s the obvious one–but we also gained a new mental model of interfaces, state, and interactions. We could stop worrying as much about encapsulating concerns–in React’s world, we can mostly take encapsulation for granted–and we started worrying more about how our work could be reused and extended.

That was the first big shift. It won’t be the last.

Laziness, as they say, is a virtue, and software developers cut corners like the grass on a putting green.

A few decades of sparring with our digital partners have taught us many clever techniques for saving time, avoiding redundancy, and sparing ourselves all but the smallest units of not-absolutely-necessary effort. Runtime environments (RTEs) are one such technique: software-defined environments that make it easier for other software to run. An RTE may manage memory, define an execution model, abstract its host system, and save a lot of trouble for everyone that doesn’t want to implement its features themselves.

JavaScript developers are well-used to RTEs. Think of the mostly-standard environment available in modern web browsers or the server-side world of Node.js, then imagine trying to put text on the screen without console.log or some variation on the DOM. In the best case, a UI kit in the host application would render the text and let you position it; in the worst, you might wind up flipping pixels on the display by hand. But–lucky you–Node and the browser both offer shortcuts to avoid all of that.

RTEs exist because they simplify a certain class of programs within a certain domain. They can cause a good deal of heartache if stretched too far, too, but for our purposes let’s assume they’re there to help.

Another virtue is stupidity.

In the usual order of things, C-style programs proceed from a predetermined entry point–often a function, often called main() –to an orderly exit with a system-specific status code. Along the way they slurp up input, flip bits, spit up output, and do whatever other useful things they’re designed to do. It’s all very linear. Very straightforward. Very stupid.

And then there’s JavaScript.

We left the Browser Wars just as JavaScript had begun its metamorphosis from curious-but-impractical novelty into still-curious-but-marginally-more-practical development platform. Internet Explorer’s XMLHttpRequest (1999) heralded a radically more dynamic world. Still, the last word, at least for the time being, was a static webpage peppered with <script> tags.

JavaScript’s formative years in a long-lived, resource-constrained environment spawned several interesting features.

First, instead of beginning from a function called main() , JavaScript programs proceed from line 1. Every script runs from top to bottom, and the interpreter evaluates every expression between. Control may or may not proceed linearly; any script can live indefinitely by listening for future events; and the script may not “exit” until a user closes the page.

A second interesting feature stems from having multiple scripts on the same page. By default, every top-level JavaScript expression is evaluated within the same, global environment. Any script anywhere on the page can access any variables set by the scripts that preceded it. This is a feature, not a bug: plugin frameworks in libraries like jQuery and Dojo, for instance, announce their presence by registering themselves to memory “owned” by their parents. But it doesn’t make it any easier to sort out where a JavaScript program begins or ends.

Finally, there’s JavaScript’s much-maligned context operator, this , which takes on different meanings depending how a function is called. In the same function it may represent the global context, a context representing a new instance of an object, or pretty much any other context that exists anywhere else in the application. Ever looked at a function and wondered, what’s this ? Without seeing the outside world, it may not be possible to tell.

Open control flows, shared memory, and opaque context, all cobbled together atop mostly-static documents. If we blew JavaScript away and rewrote it there are likely some things we’d change. Many of them are changing—JavaScript tooling modularizes, normalizes, and modernizes like it’s going out of style—yet the spectre of back-compatibility is never far away.

The problem cuts two ways. Yesterday’s Internet should work no matter what newfangled browser is accessing it, of course, but we’ve also grown more cautious about introducing changes that we’ll need to support tomorrow. Specification processes are as transparent and collaborative as they’ve ever been. By and large we’re heeding the call to extend the web forward. But thoughtful, incremental processes leave less room for a radical rethink.

Can we challenge the basic patterns of web programming without breaking the web? One possibility is to experiment with any of <x, y, or z blazing new languages> while trusting our compilers to make it all work. We’ll get better encapsulation, slicker syntax, maybe even a type-system—though we’re still interacting with the same APIs under the hood.

We could also leave the language intact but change the programming model that sits on top. This is an appealing idea, as developers wouldn’t need to learn entirely new syntax–just new (and hopefully better-adapted) ways of thinking.

We just need some way to make those changes. Which means we need a runtime.

A good runtime provides fundamental abstractions that match the problem at hand. React is oriented specifically at programs that render UI trees and respond to interactions. – Dan Abramov, “React as a UI Runtime”)

In React’s world view, user interfaces are made up of trees of components. Each component encapsulates its own rendering logic, interactions, and state, acting, in effect, as a tiny program. React is the runtime that “executes” each component, glues them together, and reconciles their output with the user-facing DOM.

React has mediated input, flushed output, and abstracted browser vagaries from day one. It’s built sophisticated user interfaces from funny little angle brackets, and it’s made those interfaces fast. What it hasn’t done, at least until recently, is change how we use the language itself.

Much of the excitement around React’s Hooks API has seized on its value for reusing logic. Squint a little harder, though, and Hooks are actually even more interesting than that.

[Hooks] let you use state and other React features without writing a class. – React Documentation

Think about that for a second. Early React components were objects stamped out by a factory function, createClass , for the simple convenience of having the component’s props , state , and methods grouped within the same context. As it became apparent that many components do not need state or lifecycle methods, React 0.14 (2015) shortened the path from props to render by introducing Stateless Function Components (SFCs). This worked because no context–and therefore no class –was needed.

… use state … without writing a class.

Let that sink in. If you heard correctly, and I’m pretty sure you did, React’s claiming that components (programs) executing inside its runtime can have context without a class . Without this.

So, what if–what if–all components could be the functional sort? There are some technical challenges, sure, but if we could solve them our components would only ever deal in explicit, local variables. They would all begin at the same entry point and return output with minimal regard for their lifecycle. Developers would only be one component pattern to learn and reason about. And there might even be opportunities to reuse common logic.

Now a sympathetic runtime has come along and given us a way. If React already put paid to the global DOM, why not iron out some of JavaScript’s complexity next? We learned to love those angle brackets, and with them a more natural way to think about user interfaces. A more natural programming model just might be next.