Designing SolidJS: JSX

How is it that the syntax born of the Virtual DOM is also secretly the best syntax for Reactive UI libraries?

Ryan Carniato
Dec 2, 2019 · 10 min read
Black and White Carbon Close Up by Engin Akyurt

SolidJS is a high-performance JavaScript UI Library. This article series goes deep into the technology and decisions that went into designing the library. You do not need to understand this content to use Solid. Today’s article focuses on Solid’s JSX templating system.


JSX may not be the most obvious choice for templating in a Reactive UI library, but it definitely brings something to the table that should not be overlooked. It allows increased flexibility, better tooling, and unmatched performance. I consider JSX to be a big part of what makes Solid the fastest reactive library out there. Some have claimed it is something that shouldn’t be matched, but I challenge you, the reader, after reading this article to suggest why would they ever not.


JSX in a Nutshell

JSX was developed by Facebook as a means to make React’s imperative API declarative. Reacts approach to rendering isn’t unlike a procedure you might find in a video game engine. It just calls the function over and over again to render the screen.

React.createElement("div", { id: "main" }, [
"Hi ",
React.createElement("span", {}, [state.name])
]);

JSX allows the developer to instead write:

<div id="main">Hi <span>{state.name}</span></div>

This approach to compiling JSX to JavaScript got standardized as HyperScript often denoted by h where any library can provide their factory function and take advantage of JSX. This simple approach opened up JSX to a number of libraries, as all you needed was to handle a function that accepted 3 parameters, the tag or Component, the props, and the children. You can write your own DOM library that leverages JSX today by writing a simple function that can handle this.

import { h } from "your-library";h("div", { id: "main" }, [
"Hi ",
h("span", {}, [state.name])
]);

Classic Reactive Templating

Templating for Fine-Grained Reactive UI Libraries, ones built off of atomic state updates instead of differential Virtual DOM has been the domain of string templates. This started with a history of being applied on top of HTML, and server-side rendered in the early days before the prevalence of Single Page Apps. Beyond being easy to seamlessly place on top of rendered HTML strings, string templates have a few critical advantages for these Reactive libraries.

Firstly, they obscuring dedicated Reactive data objects and computations. It looks a lot nicer to just reference a variable than to execute an accessor function(observable) or perhaps wrap the whole binding in a function. Remember, in a fine-grained library like Solid, tracking scope is everything. So your binding expressions cannot just be executed in the open.

Secondly, these strings represent HTML in general. Meaning that the range of possible values and inputs is very restrictive. You can bind to attributes or properties, but conditionals and loops required a specific invented syntax or DSL (Domain Specific Language). This puts complete control in the parser and library to handle and specify what is legal.

Finally, you can see the whole template ahead of time and structurally optimize the code execution. This templating does not have an imperative underpinning. It isn’t just function calls. And it can leverage that fact to look ahead before running any code. The result is completely abstracting the underpinnings of the library for the end-user.

This definitely is a way to keep things easy when in reality the libraries are setting up a dependency graph in the background, wired to dynamically handle all your change propagation needs. But it comes at a cost. The most egregious one is the need for an artificial scope. Instead of being able to use local scope data objects need to be constructed to carry the scope and define new scopes inside loops etc. This not only adds overhead but it can get in the way of tooling like ESLint or TypeScript. Some people would also point out the required DSL as a drawback but for fine-grained I’m pretty neutral on that as you need specialized methods to deal with the reactive data efficiently anyway. I would point out that lack of expandability is more of the issue with the DSL. There are libraries that allow you to write not only custom binding directives but custom control flow directives so that isn’t completely true. It’s more than the process of doing so becomes more of an effort.

So in my typical way, I refused to accept that I couldn’t do something to make this better. Resolving the scope/context issue was my starting point.


JSX in Solid

Masking Reactive Data

JSX offers something really useful here. A predefined AST syntax that was standardized and compatible with existing tools. So now it was just a matter of handling the things that you take for granted with string templates.

Pretty early on I made the decision to use Proxies as the primary state objects. I wanted to avoid the accessor functions where it made sense. I saw the benefit instate.count over count() when dealing with multiple state atoms. Luckily using Babel with JSX I could turn the expressions into functions. So the JSX above could be compiled to:

import { h } from "your-library";h("div", { id: "main" }, [
"Hi ",
h("span", {}, [() => state.name])
]);

This allows the access of state.name to happen in its own scope. This allows just that expression to re-evaluate without re-executing the whole tree. I considered Tag Template Literals for a bit but they would push the wrapping of access on the end-user. So just like the String templates, preprocessing was the way to go.

It seems simple enough and I could have stopped there but this approach while mostly compatible is completely inefficient. Think about it for a second. Sure we do not need to re-render unnecessary parts here due to our fine-grained reactivity, but what happens when we are running big chunks of view code, perhaps repeated rows in a list?

Escaping HyperScript

Now Solid does support HyperScript versions if people wish to use it and they can use Tagged Template Literals or traditional JSX compilers with it, but these approaches will likely never be the best for reactive libraries. The reason is that they are runtime approaches. If we aren’t doing heavy diffs or constructing a virtual DOM tree, why bother?

The first thing you should be thinking about is all the extra function calls and prop object creations. The truth is that the h factory needs to parse and determine the best way to handle the input of every execution. If we are compiling anyway we already know the shape.

Let’s look at the HyperScript again:

import { h } from "your-library";h("div", { id: "main" }, [
"Hi ",
h("span", {}, [() => state.name])
]);

The only dynamic part is state.name the rest of this is basically static HTML. HyperScript is breaking apart that HTML into multiple methods, which means at best we are doing a bunch of document.createElement calls on creation. Cloning a whole template of nodes is much more performant.

You might also note the inner h finishes executing before the outer one does. That span doesn’t know who its parent is at the time of being created. HyperScript executes inside out basically. This is perfectly acceptable for a runtime approach that does a double pass over the data like say rendering a virtual tree and then diff patching. But for a single pass library, this can be restricting.

I mean what we really want is something much much simpler. Conceptually we are looking for:

const div = <div />

Why can’t a <div /> just be an HTMLDivElement? Why all extra. Ok, maybe not exactly that easy perhaps with dynamic bindings but not necessarily any more complicated. So what if instead, the JSX example in the previous section compiled down to:

// do once on module load
const tmpl = document.createElement("template")
tmpl.innerHTML = `<div id="main">Hi <span></span></div>`
// inside your component, run as many times as you want
const div = tmpl.content.firstChild.cloneNode(true);
const span = div.firstChild.nextSibling;
createEffect(() => span.textContent = state.name);

The setup time did cost us an innerHTML. However, the runtime code generation is much more efficient. createEffect is Solid’s version of a reactive computation and it wraps that single update. But outside of that, there are no additional closures, no parsing, and you basically see all the relevant code in front of you. It’s not only much more efficient, but it’s also more transparent. That is all the code that runs when this view renders. Short of having to debug into createEffect what you see is exactly what you get. This is what Svelte refers to as a disappearing runtime. But there is still a small runtime, but that level of surface simplicity can make it really easy to see where bugs are.

Components and Context

Remember a moment ago I mentioned that HyperScript executes inside out. Well, this is a real thing for JSX in general because it’s just JavaScript so we can insert JSX in JSX in JSX and so on… Full-on inception here. No matter how I compile it, it is going to end up being a function call of some sort that executes its children before it finishes. Especially when you consider Components. One thing that I’m already doing is wrapping expressions in functions, so the parent can at least choose when to evaluate any dynamic children. But I wanted Dependency Injection like React’s Context API.

Typically a Reactive library ties it’s context to its DOM nodes so it’s pretty natural to do the same for Components. The challenge there is with JSX creating children before attaching to the parent you cannot walk up the DOM tree unless you do a second pass. Like an “onConnected” or “onMounted” lifecycle hook, and I knew I wanted to avoid that unnecessary overhead when a Reactive graph makes lifecycles redundant for the most part. I could defer Component execution until attaching to the parent, but that would add a decent amount of complexity and prevent other things that I wanted to explore (like Suspense).

So instead I did something pretty unprecedented in reactive libraries. I used the hierarchy of the Reactive graph to store contextual data. The way a Fine-Grained Reactive system with auto-dependency tracking works is that in each execution context the currently running computation(reaction) is hoisted to a global scope where any reactive access under it can be assigned. So all I needed to do is backlink each context to its parent. By storing values on it any child context can do a lookup up the tree at execution time to find this value. It works very similar to React’s Context API in that the reactive graph is a sort of virtual tree, and it has no basis on the actual DOM, and can be accessed even when detached.

What this means for JSX though is that the rendering all happens in memory first as execution goes down the tree, and then attaches to its parent on the way back up. So it is only attached to the Real DOM when the whole page has been rendered (outside of asynchronous effects).

Control Flow

This is really the final piece. Understanding how Components and Context work we can use them to manage conditionals and loops in the view. For better or for worse with a reactive library, you are dealing with specialized data-types. No matter how you mask it they are there and in so things like native built-in array methods will never be optimal. The string DSL of reactive libraries hides this fact as well. Using Components isn’t unprecedented at all (see https://github.com/leebyron/react-loops) even with React although Solid using this approach well predates this. While it’s a DSL of sorts it isn’t fixed. The big win here is we are still using real JavaScript scope. We get to leverage tools like ESLint or TypeScript.


Challenges

For all the benefits it isn’t without a few challenges. The hardest ironically has been TypeScript. One of the key benefits is that this approach is completely type-able out of the box. Or at least in theory. The reality is TypeScript basically assumes if you use JSX you are using React and has been very slow supporting other libraries even other HyperScript ones. I’ve opened issues and weighed in on almost a dozen issues at this point. There are some shortcomings here. Nothing unsurmountable but definitely awkward. Like there is no easy way to tell TypeScript <div /> is an HTMLDivElement without casting.

Unfortunately for me, this was my first introduction to TypeScript working on Solid, so I have to say it felt incredibly not mature at this point. But all Solid is written in TypeScript with TypeScript in mind, so I’m betting on with enough support they will eventually fix these sorts of issues.


Conclusion

If you been following up to this point you might start to see the power of putting all these things together. JSX being JS means that DSLs are arbitrary. Custom Bindings are no harder than just using ref and more so you can create custom control flow. By that, I mean things like conditionals and loops. Not many libraries let you write your own if or each bindings. With this approach, you can manage control flow however you want. I’ve used this to write things like Portals, Suspense, Switch/Case, Lazy Loaded Components, whatever is needed.

My favourite part might be is how easy it is to make partials. You can split Components and refactor very easily. Even just splitting things down in a single file like React with such ease is not typical for these libraries. It can be done at no real cost. This leads to greater code clarity and maintainability. When it comes to long term project sanity React gets a lot of things right.

I also realized this approach was generalizable to any reactive library. I’ve made versions of this core compiler/renderer for MobX and Knockout as well. Both perform incredibly well even if not as fast as Solid they are still among the fastest libraries going head to head with Inferno and the like well ahead of any of the more popular reactive libraries like Svelte or Vue. (See JS Framework Benchmark)

Obviously JSX is not the only way to achieve this, but it is definitely more dynamic. Once your string template parser starts needing a JS parser anyway you might consider giving JSX a try too.


JavaScript in Plain English

Learn the web's most important programming language.

Ryan Carniato

Written by

FrontEnd JS Performance Enthusiast and Long Time Super Fan of Fine Grained Reactive Programming. Author of SolidJS UI Library.

JavaScript in Plain English

Learn the web's most important programming language.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade