alexkritchevsky

a blogaboutatom

The Zen of React

September 17, 2022

React.js, that is.

I am planning to write a series of posts about React: 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.


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 wonderfully direct because 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, it’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? Because… 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 the latter, HTML, for the foreseeable future.

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 based on 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.

The XML syntax doesn’t limit you to declarative programming, either. You can implement control-flow in XML: just create tags like <if> and <else>. You just define the entire syntax tree 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, to define custom elements that expanded into user-defined 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. When you see it written out in the imperative syntax above it’s obvious; it’s begging to let you define custom elements as a function. It’s still just a declarative representation of data, just, with the ability to define variables. 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, 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 just writing it verbatim, you could freely use PHP’s ability to abstract to invoke functions inside of the HTML generation. Great, but limited to the server-side, so it doesn’t solve dynamically updating in the browser.

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:

$body.append($("<div>").attr('class', 'specialButton').click(doSomething))

But it doesn’t scale. Anyone who has worked on an app built this way knows 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). Invariably the deltas 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, every time, to ruin.

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.3


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 unless we threw HTML out, 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. It’s okay if the language also supports imperative styles, if you’re not using them (although you surely will… more on that another day).


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 partly 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!

And then you can just 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!

And 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>;
);

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.

Well, okay, it’s definitely not perfect. Far from it. There are rough edges everywhere , and writing clean and bug-free code is still 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 fantastic.

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 articles about React:

  1. The Zen of React
  2. The Gist of Hooks
  3. React Mistakes
  1. except for that doSomething() bit 

  2. XML does have something truly awful which looks kinda like variables. Heh. 

  3. Aside: nowadays browsers also support something called custom elements which lets you define new HTML tags in JS. It’s clearly inspired by React; they even have React-style lifecycle methods! But it’s probably too late. React has already moved on from class components, and for good reason. But maybe somehow these will end up being the future?