I dislike "magical thinking" in software development and React has become too magical.

I've been a React developer since 2015 so I went through the "class-based to function-based" revolution in React Development. I've gone through the iterations of learning about Hooks, and Memos, and Callbacks, and I have to say that modern React requires a daunting amount of experience to write with any competence as well as a deeper understanding of JavaScript's call-by-value vs call-by-reference nature to avoid re-renders.

So it was with the typical glee of confirmation bias that I read Nudge's React is Holding Me Hostage in which he outlines the problems with modern React, including the illusion that state in React is a part of the component when, in fact, it is an input to the component.

Unfortunately it looks like the Lit team is making the same mistake.

Coming as I do from the Old Skool, where we hungrily read the O'Reilly DHTML book and memorized the contents of the DOM Node object and its descendents for any clues as to how to make this thing look better and go faster, I found myself nodding along as I realized that Functional React has created a divorce between the rendering functions, the VDOM, and the DOM. It is hard (I might almost say "impossible") when looking at a React function to convince myself that it's a persistent thing, that it has an existence on the page that represents the state of the web application. There's just too much magic between the functions I write and the DOM object I care about.

I had hoped Lit and Web Components would help.

What's a Web Component?

Skip ahead if you already know.

Lit is based on Web Components. The Document Object Model is the native, internal description of an HTML document to the browser, and since the mid-2000s browsers have had an internal API for describing derived HTML tags that don't need a native implementation; for example, the <em> component, which causes text to be bold, is implemented as a <span> with its style set to a heavier font weight. Older (and much unloved) tags like <blink> and <marquee> are implemented as <div>-elements with JavaScript attached to them to create their much-hated behaviors.

Web Components formalize and expose that internal API to the world. You and I can now write our own HTML tags, complete with whatever behaviors we want. It's a great power and comes with the corresponding great responsibility, especially when implementing our own inputs. Because Web Components use a native API rather than trying to replace it, they can be very fast indeed. And because each instance of a component is-a DOM Node the mental model for their lifecycle is already familiar to experienced web developers.1

Web Components are actually three different things: The custom element, which lets you define a new tag and all the expected behaviors of your nifty new DOM Node derivative; a template, which is a miniature DOM object of many tags that you can use to describe the layout of your new component; and the shadow DOM, which allows you to isolate the CSS of your component, so that users of your component can't break it with their own CSS but can still get things like color, font, and size in if you care to grant them.

Lit-HTML allows you to create Web Components easily by providing toolkits for managing the Shadow CSS (via the css function) and Templates (via the html function)... or at least, that's how it started.

Lit Takes Hostages

Lit-Element promises to be an even easier way of creating Web Components; it provides decorators to encapsulate some of the more annoying and verbose tasks of defining a new component, as well as alternative names of many of the Web Component lifecycle stages. Web Components have their own versions of react's mount/unmount, willupdate/didupdate, but the names are quite long because of their legacy history. Names like connectedCallback and attributeChangedCallback can become annoying to write over and over, although modern IDEs help a lot in that regard. (I supposed they're not really all that long compared to React's shouldComponentUpdate or componentDidMount.

The problem, and maybe it's just poor documentation, is that Lit is suffering for React Envy. A Web Component is an HTML Component-- all the attributes, like those in HTML, must be strings, and the only way to get complex properties into an HTML component is to find it via the classic DOM lookups such as GetElementById() after which you can call its methods to send it objects.

Lit has a syntax that allows you to treat non-string properties like attributes and it has an underlying renderer and scheduling algorithm for batch-updating your elements, calling the internal render() function on each Lit-Element for which the state has changed and leaving the rest of them alone, just like React.

Just like React.

No longer do you know when a Web Component will update; it will update when Lit is good and ready to do so, and not a moment before. All of the modern React issues with caching and memoization come roaring back in the Lit-Element framework when software engineers decide they know better than their users, and better than the browser, how the web Should Be Done.

Which leads us to the current issue with the Lit-Element animate() function. Because Lit wants to re-render the entire web component when its state changes, the CSS associated with that component will be refreshed instantly, which makes writing transitions for Lit components much harder than it should be.

Take, for example, the following: "I have a word logo; I would like each letter of the logo to shrink, fade, and slide downward when I click on it." Normally, this would be a matter of writing a start class and an animation class with a couple of keyframes to describe the shrinking, fading, and moving transitions; activating the animation would be a matter of swapping the start class with the animation class.

Lit doesn't want you to touch the class definitions inside your web component; "going around" the render() function messes with their scheduling. But calling render() creates new nodes inside the DOM, which screws up the browser's understanding of how a transition is to be run.

So Lit provides the animate() function, which somehow, magically, messes with the way the render() function works. Which makes my example, which is just some mid-level CSS, look like this monstrosity:

render() {
    const delayTime = this.duration / (this.letters.length * 2.5);
    return html`
      ${this.letters?.map((letter, i) => html`<span class="letter"
            ${animate({
              keyframeOptions: { delay: i * delayTime, },
              in: fade,
              out: flyBelow,
            })}>${letter}</span>`)}
    `;
}

I have no idea how the animate() function works. The documentation says it creates "tweened animations" between any two points of the animation and uses Lit's rendering engine to guarantee that the animations are smooth and accurate. Which is all well and good, but that means that there's yet another programming language, the DSL of animate(), that I have to master above and beyond CSS transitions and keyframes.

And I have no idea how this would work with things like perspective or SVG or WebGL. Have the authors of animate() gone to the lengths of including those technologies in the things that we can animate, or are we going to have to use refs again (and Lit has refs, because not every bit of magic has been thought about in advance) and "route around" the rendering architecture just as we did with React?

I Want to Believe

I actually do want to believe that Lit-HTML and Lit-Element are superior to both React and to Web Components, but the animation thing puts me off so much that I'm not sure what to do with them. I am tempted to look elsewhere; Slim is a library that takes a declarative approach at a lower level, while Stencil provides lots of goodies like state and reactivity without promising to control your entire world for you, and its magic lies in that your Typescript definition files are actually a DSL and the output is a fully realized Web Component, no library required. That's magic I can see, magic only one step away from reality, and I appreciate it for that.

I do believe that moving away from React will be good for the web. I just don't know if Lit is headed in the right direction.


1 I remember reading an article that praised React but included this note:

Reasons your team may not want to use React:

  • You have frontend designers familiar with HTML.
  • You have frontend designers familiar with CSS.

Lovely, huh?