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.
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.
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
alerts to pop up,
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 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
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.
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.
environment available in modern web browsers or the server-side world of
Node.js, then imagine trying to put text on the screen without
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.
curious-but-impractical novelty into
still-curious-but-marginally-more-practical development platform. Internet
XMLHttpRequest (1999) heralded a radically more dynamic world.
Still, the last word, at least for the time being, was a static webpage
First, instead of beginning from a function called
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.
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,
this? Without seeing the outside world, it may not be possible to
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.
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.
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
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
introducing Stateless Function
(SFCs). This worked because no context–and therefore no
… 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
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.