The Zen of React
I am hoping to write a series of posts about React.js: why it’s great, why hooks are great but also confusing, and then maybe what all is wrong with it and what can be done. These are largely perspectives I came to hold while working as a frontend developer at Dropbox for the last few years (I quit earlier this year, though, for… reasons.)
1
In the beginning there was HTML:
<html>
<body>
<p>Want to press the button?</p>
<div class="specialButton" onclick="doSomething()">Click me</div>
<div class="label">Do it</div>
</body>
</html>
HTML has a certain elegance to it1, which is that you pretty much write exactly what you want to see on the page. The syntax is cludgy, and the output looks like crap by default, but it’s what you asked for — in this case, a document which contains some text, a button, and a label.
HTML is refreshingly simple. That reason it’s simple is that it’s declarative: there is no mechanism in plain HTML code for running arbitrary functions and loops and whatnot, in which it would get to do anything complicated. Declarative code is just easy to read and write.
Of course, there’s a tradeoff: is it better to use a more expressive language that can do everything you need, or to use a simple declarative one that does what it says on the tin but can’t do very much? Because… if you do use the declarative language, you will have to build a whole ecosystem in other languages around it.
Well, it doesn’t matter, because in 2022 we’re stuck with a declarative core, HTML, for the foreseeable future, because that’s how browsers. There is absolutely no reason that that has to be how browsers work, and my money’s on there eventually being browsers that experiment with entirely new languages for making websites, but so far it doesn’t seem to be happening very quickly.
It’s worth noting that HTML did not need to have an XML-based syntax to be declarative. We could just as easily have built the whole web on a language that uses a function-call syntax which looks like Javascript:
html(
body(
div("Click me", class="specialButton", onclick="doSomething()",
div("Do it", class="label")
)
)
Alternatively we could have used something like JSON?
document = {
type: "html",
children: [{
type: "body",
children: [
{type: "div", class: "specialButton", onclick: doSomething, children=["Click me"]},
{type: "div", class: "label", children: ["Do it"]},
]
}]
}
Bit awkward. Maybe Lisp?
(html
(body
(div (class "specialButton") (onclick "doSomething()") "Click me")
(div (class "label") "Do it")
)
)
Or whatever. It’s all the same if you limit the syntax appropriately, and if the browser interprets it as the same result. If history had gone a bit differently, we could have been using any of these.
Nor does an XML syntax doesn’t limit you to declarative programming. You can implement control-flow in XML: just create tags like <if>
and <else>
and implement them appropriately in the XML engine. Then you could just define the entire syntax tree of your arbitrary imperative code as XML entities, like Java’s ANT build system regrettably did.
So HTML is beautifully simple because it is declarative, in that it describes the logical layout of the page and the browser goes and figures out how to build it, and because nobody has messed it up by bolting control flow onto it.
Which would be fine, except for one major problem that was never really solved: abstraction.
2
The thing HTML didn’t have and always needed was the ability to abstract nodes out, like this:
specialButton = (doSomething) => div("Click me", class="button". onclick="doSomething")
That is, it never had a good way of defining custom elements which would be expanded into subtrees, while keeping in the declarative style.
As soon as you do anything non-trivial in HTML you just end up wanting to re-use subtrees of nodes. When you see it written out in the imperative syntax (like my JS example above) it’s totally obvious that you would want to write custom elements as something-like-a-function. It would still be declarative, just, with the ability to define abstractions also. But nope! HTML has never let you do that.
An imperative language without function definitions would be absurd. But for some reason when you use a language with an XML-based syntax everyone forgets that the ability to abstract is totally fundamental to programming.2 Even in full-fledged XML you only define tags in separate “schema” definitions, not inline with the actual code you’re writing. What the heck is that?
From the beginning HTML’s use for complex development was hamstrung by its lack of support for abstraction. A bunch of things were tried instead:
1. CSS: CSS is a glued-on way of implementing abstraction in HTML, but only for style
attributes. Two divs with the same specialDiv
classname have something in common: they’re both ‘inheriting’ some shared style information (a sort of mixin inheritance). It’s powerful for what it does, but when you view it as an attempt at solving “abstraction in HTML”, it’s far from adequate.
2. Server-side templating: Server-side HTML document construction used to be the bread-and-butter of web-development. Run some SQL queries, iterate over the output, create HTML corresponding to each thing you read, accidentally parse a bunch of user data as JS and let it run arbitrary code in the browser, send over the wire…
In a language like PHP, since you were generating HTML instead of writing it verbatim, you could freely use PHP’s ability to abstract to invoke functions inside of the HTML generation. It works great, but limited to the server-side, so it doesn’t solve dynamically updating in the browser. Also, mixing languages together in one file is always worse (in terms of development abstractions like syntax highlighting and debugging) than working in one language at a time.
3. Imperative UI: Of course you can always generate dynamic, abstractable HTML in Javascript code directly. That’s what JQuery ended up being used for a lot of the time:
$body.append($("<div>").attr('class', 'specialButton').click(doSomething))
But it doesn’t scale. If you’ve ever worked on an app built this way you know how it degenerates into eligibility.
Specifically, the reason it fails is that it loses all the affordances of declarativity: any change you make, you have to apply manually, and unapply manually afterwards, so you end up writing a bunch of deltas between states (A->B and B->A) instead of just the states you want directly (A and B). Also, the deltas invariably interfere with each other, and invariably debugging what happened is madness. And that’s before considering the endless temptation to mix, oh, just a bit of stateful business logic into the document generation – which leads, inexorably, to ruin.
It turns out that if what you want to do is have a data structure that switches from state A to state B, you want to just write out the two states explicitly, and not the deltas between them. At this point that it simply indisputable.
4. Templates: Did you know that every major browser supports HTML Templates? They look like this:
<template id="specialbutton">
<div class="button">Click me</div>
</template>
Unfortunately, to use them, you have to clone, modify, and insert them from JS:
const template = document.querySelector('#specialButton');
const clone = template.content.cloneNode(true); // ok...
clone.firstElementChild.onclick = doSomething; // wtf
body.appendChild(clone); // sigh
A nice idea but in practice, a total pain, and not declarative at all except for the template itself.[^btw]
5. Custom Elements: Nowadays browsers also support something called custom elements which lets you define new HTML tags in JS. This one is actually what we always wanted. If they had existed a long time ago React may have never been written. But they don’t count for the purpose of this article, because they’re new.
Custom elements are clearly inspired by React. They even have React-style lifecycle methods! But it’s too late. React has already moved on from class components, and for good reason, because they’re not actually the best way to do abstractions, they’re just the first way you think of (if you’re steeped in OOP). But maybe somehow these will end up being the future after some iteration?
Look at them all: these feeble attempts at adding abstraction to HTML. We were floundering and suffering… and then along came React, and more importantly, JSX, and let us do this:
const SpecialButton = ({onClick}) => {
return <div class="specialButton" onClick={onClick}>Click me</div>;
}
Would you look at that. It’s all we ever wanted!
Yes, we have to start writing our UI in JS. But that (a) was always going to be necessary, and (b) doesn’t mean it’s not declarative, because it’s just a syntax. Declarativity is ultimately about whether you are directly writing out the data as you want to see it; React proves that it is possible to munge this into an imperative language that executes along a “time axis”. It’s okay if the language also supports imperative styles, if you’re not using them (although you surely will end up splitting the difference).
3
The Zen of React is that you just write down what you want to see on the page, just like HTML, but with abstraction, which lets you write composable, maintainable, scrutable UI.
(The non-Zen of React is how much munging it takes to get it to work afterwards. But hey, at least it’s somewhat zen.)
Want to scaffold out a new website but you don’t know anything about what it looks like yet? Just start writing out the component structure and figure it out as you go:
const App = () => (
<UI>
<Header />
<Sidebar />
<Content />
<Footer />
</UI>
); // close enough!
When you’re done with your template, jump into implementing stuff as your heart desires:
const Header = () => {
return <div class="header">
<h1>My cool app</h1>
<Button>About</Button>
<Button>Sign in</Button>
</div>;
}
const Sidebar = () => {
return <div class="sidebar">
<Button>Home</Button>
<Button>Whatever</Button>
<Button>Else</Button>
<Button>A</Button>
<Button>Sidebar</Button>
<Button>Needs</Button>
{/* etc, live your life*/}
</div>;
}
// keep going, just write that UI out and fill it in later!
Writing the same code over and over? You can make reuseable abstractions out of anything you want. Maybe you find yourself wanting to define a new component that’s a button with a label, so that every button on your site is an instance of the same components that work the same way? Yes, why not:
// in practice these reuseable components are way longer because they take care of
// accessibility, logging, animations, responsiveness, etc.
// good thing you just write it once and reuse it everywhere!
const ReusableButtonWithLabel = ({text, onClick, label}) => (
<div>
<div class="button" role="button" onclick={onClick}>{text}</div>
<div class="label">{label}</div>
</div>;
);
And React gives you the tools to do almost anything you could want. Want to make a reuseable modal that pops out of a widget and overlays the whole page but still bubbles events to its parents without architected your whole app around it? Heck, sure, and unlike every previous way to do this, it’s declarative, even though it involves jumping across the DOM:
const WidgetWithModal = () => {
const [modalOpen, setModalOpen] = React.useState(false);
return
(<div>
<ReusableButtonWithLabel
label="Want to open a modal?"
text="Click me, yeah"
onClick={() => setModalOpen(true)}
/>
{modalOpen &&
(<Modal onClose={() => setModalOpen(false)}>
<div>Yeah, I'm in a modal!</div>
<div class="button" onClick={() => setModalOpen(false)}>JK</div>
</Modal>)
}
</div>);
}
const Modal = ({onClose, children}) => {
// okay, this part is a bit awkward
const container = React.useMemo(() => document.querySelector(".modal-container"), []);
// render a gray overlay over the whole page, then put the modal
// contents on top of it.
// no one knows why the syntax for this isn't <Portal>
return React.createPortal(
(
<div class="overlay" onClick={onClose}>
<div class="modal-body">
{children}
</div>
</div>
), container);
}
It is really so great.
Okay, yeah, it’s not perfect. Far from it. Nobody really wants a giant runtime library with a second shadow DOM to do any of this. In the long run I’m guessing that part goes away. And there are rough edges everywhere, and writing clean and bug-free code is still really hard and often requires deep experience and actual expertise to get right.
But it’s so much closer to what development should feel like, compared to what came before, that it’s still fantastic in my book. I like to say that programming is like… 15% of the way to what it will look like in 100 years, and React pushes it to like 20%. React with only hooks gets maybe as far as 25%.
And the more it evolves to deliver on its promise, the further down the path to programming nirvana we get. (Hopefully one day there is no HTML anymore and JSX is directly rendered by browsers. Just saying.) It dramatically expands the scope of “what can be done declaratively”, which is what we needed the whole time even if we apparently we didn’t realize it.
Unfortunately there is lots of tricky business that goes into massaging it into exactly what you wanted and working around weird limitations and of course handling state. But to even be able to write it out in the first place — that was the big innovation of React.
The rest of React: components, props, reconciliation, shadow DOMs, contexts, lifecycles, hydration, Devtools, even hooks… these are just there to make the zen part, actual composable UI, work. The other parts are all a mess and in 20 years will probably look nothing like they do now. But at least we got JSX. Even if it’s clunky.
My other articles about React:
-
except for that
onclick
bit ↩ -
XML does have something truly awful which looks kinda like variables. Heh. But I don’t think HTML does. ↩