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, <a
href="https://overreacted.io/react-as-a-ui-runtime/" target="_blank">“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 appropraite 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.