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 in state.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.