The Zen of React

[September 17, 2022]

React.js, that is.

I am writing 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: 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 just begs to let you define custom elements as function, like specialButton = (doSomething) => div("Click me", class="button". onclick="doSomething"). It’s still just a declarative representation of data, just, with the ability to define and use 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? Go for it:

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

Don’t get me wrong: it’s 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 zen 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.

Of course 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.


Appendix: the zen parts

JSX is zen. Writing exactly what you want to see and then adding details later is zen. Clean abstractions that let you keep doing that are also zen.

Here are some other things that are fairly zen:

  1. Function components
  2. Hooks, most of the time
  3. Contexts
  4. Portals
  5. View-only components with no state
  6. Logic-only components with no views
  7. Components with no UI at all that just give definite lifecycles to other things, like Redux stores or network clients or caches.
  8. Component libraries
  9. Design systems which abstract out and ‘solve’ common functionality like buttons, menus, and modals.
  10. Inline styles, especially for non-inherited concepts
  11. Error boundaries
  12. Async components with <Suspense>
  13. Hiding things with style={{display: "none"}} instead of unmounting them, if they’re conceptually invisible instead of gone.
  14. Unit testing with React Testing Library
  15. Hydration, in principle

And here are some things that are currently not zen. Not all of those are React’s fault. Some if it is just how browsers work and some of it is historical baggage. Nevertheless:

  1. Class components
  2. Redux
  3. Iframes
  4. Animations / Transitions
  5. HOCs
  6. Smooth user interaction, such as draggable elements
  7. Keybindings
  8. Focus and focus-locking
  9. Handling tab ordering and screen-reader ordering
  10. Z-indexes
  11. SVG, Canvas, MathML, and WebGL
  12. ResizeObserver
  13. Refs in general
  14. But especially useImperativeHandle
  15. Cross-app interactions, such as a button in a sidebar controlling the main view.
  16. The syntax for Portals
  17. Having to implement ErrorBoundary class components.
  18. Chasing down React component errors once ErrorBoundary catches them
  19. Figuring out what is re-rendering and why
  20. Inspecting and modifying props in Devtools
  21. Actually pretty much all debugging.
  22. Unit testing with Enzyme
  23. Hydration, in practice

Yup. More on some of this later.

  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?