Bringing JSX to Template Literals

Andrea Giammarchi
7 min readJun 15, 2021

Update

I’ve managed to create, thanks to Nicolò Ribaudo, a Babel plugin that solves the issue with static attributes in JSX regular HTML node.

Probably not everyone knows that, React a part, JSX can be used with pretty much any library.

The transpilation is quite simple: let’s see an example!

// this
const jsx = (
<div id="test">
<Component key={"value"} />
</div>
);
// becomes this
const jsx = callback(
// tag name or component
"div",
// all properties (attributes)
{id: "test"},
// zero, one, or more children
callback(Component, {
key: "value"
})
);

The fragment logic is no exception, it’s still a similar signature, except fragments usually don’t have attributes and, by default, React uses a different reference, as component, called React.Fragment . In all other cases, the default callback is React.createElement , but JSX is flexible enough to let us decide the name of both cases, through the following comments:

/** @jsx myCreateElement */
/** @jsxFrag myFragment */

If we are using .babelrc instead, pragma and pragmaFrag would do the trick, as explained in this wonderful post.

Before we move forward, it’s important to notice the order every callback is executed/invoked, which is inside-out, so that the most nested tag or component will be executed first, which is quite the opposite of how the DOM resolves or works, where each outer element, and related attributes, are resolved while crawling the tree inward.

About Template Literals

There’s a plethora of libraries out there, based either on plain template literals, or template literals’ tags, and µhtml is just one of these, but fear not, what I am going to show you would work literally everywhere.

Taking the previous JSX example, this is, theoretically, how a template literals based library would try to reproduce similar results but without needing any tool, transpilation, or build step:

const tpl = html`
<div id="test">
<Component key=${value} />
</div>
`;

Instead of being a callback with a tag name or component, all props, and possible children, tags are usually like this:

const html = (template, ...values) => { /*JIT*/ };

The template would contain all the static parts of the literal, while zero, one, or more values would be what we call interpolated values.

Tagged Literals Pros

  • the template argument is unique per scope/expression, hence usable, via a WeakMap, to perform a lot of operations once and once only, to understand, snapshot, and produce, the desired DOM structure, and simply cloneNode(true) any time the same template is used again (aka: Just In Time parsing)
  • values can be properly mapped, passed along, attached to elements, be used via libraries, set as listeners, and do everything we could do with regular JavaScript, with an ordered correlation with the surrounding parts of the template.

Because of these template literals’ peculiarities, libraries based on this approach are extremely fast, require a very low amount of memory, and could completely skip/avoid vDOM indirection, diffing directly on the live DOM, whenever the layout is meant to be reactive, or simply creating one-off content.

Tagged Literals Cons

  • it’s “literally” impossible to use components within the static part of the template, unless we use convoluted indirection, runtime manipulation, post-processing analysis, and so on …
  • the only way to circumvent the previous limitation is to use Custom Elements in the template, but because CE, or builtin extends, are not supported natively in all browsers, a polyfill is needed

In regard to both points, please allow me to show some previous work:

  • heresy, which requires to define components by name, and it uses template parsing to discover these names on the fly
  • µland, which cannot use components in the static part of the template, so you need to invoke these as interpolation, which works fine, but it’s not super natural
  • kaboobie, which allows µland components as interpolations, but through Custom Elements based indirection (works, but it’s ugly underneath)
  • µbe, which is my most recent, and sane, attempt to define any component, without needing a registry, without ever clashing, without needing Custom Elements, and without indirections
import {HTML, render, html} from 'ube';
export default class Div extends HTML.Div {
upgradedCallback() {
const {hello: name} = this.dataset;
render(this, html`Hello <strong>${name}</strong>`);
}
}


// ... rendering file ...
import {render, html} from 'ube';
import Div from './div-component.js';

render(document.body, html`<${Div} data-hello="µbe" />`);

… alternatives …

Assuming we understand the issue, and because we know the solution is being able to target and upgrade our components, similarly to how native Custom Elements do live on the DOM, there are ways to lazy load, upgrade, or workaround these template literals’ limitations, including:

  • element-notifier, and its dependents modules, which passes along any connected, or disconnected, element through the document, or, optionally, shadow roots
  • qsa-observer, and its dependents modules, which is based on element-notifier, but it triggers notifications via CSS selectors instead, simplifying updates per component “kind
  • uce-loader, which can lazy-load custom elements on demand, and yet, it’s custom elements dependent

Long story short: we have tons of building blocks to circumvent template literals limitations, and yet, somehow, none of these solutions is as straight forward, flexible, or simple, as using JSX would be instead … but don’t worry, I’m getting there!

Creating Tagged Literals’ Arguments

What we knew or learned, so far, is that template tags require:

  • an always same, unique, template argument with static parts
  • zero, one, or more extra arguments as interpolated values

In order to convert JSX into such signature, we could write something like:

/** @jsx callback *//** @jsxFrag callback */const callback = (tagName, props, ...children) => {
const template = [`<${tagName}`];
const values = [];
let i = 0;
// create attributes as interpolations
for (const [key, value] of Object.entries(props)) {
template[i] += ` ${key}="`;
values.push(value);
i = template.push('"') - 1;
}
// close the tag from last chunk
template[i] += '>';
// inner content
values.push(children);
// closing tag
template.push(`</${tagName}>`);
return [template].concat(values);
};
// test arguments
console.log(<div id="test">Hello!</div>);

… and test it live

The log will show an array containing:

  • a template like ['<div id="', '">', '</div>']
  • one interpolated value as "test"
  • another interpolated value which is an array of children, in this case just ["Hello!"] as text content

The only missing bit, is to ensure such template is recognized as unique, so that libraries won’t keep parsing it and create its related DOM fragment to clone each time.

Using some unique id, or special char that could not be found in the static parts of the JSX would do already a good enough job to map the template as string.

const cache = new Map;
if (!cache.has(template.join('<☠>')))
cache.set(template.join('<☠>'), template);
// now we can use that
cache.get(template.join('<☠>'));
// so that its identity will be always the same

Understanding Components

Well, this part is pretty easy: if the first argument is not a string, we assume it’s a component:

const callback = (tagName, props, ...children) => {
if (typeof tagName === 'function')
return tagName({...props, children});
// ... rest of previous code
};

The only gotcha, at this point, is recognizing the Fragment, but since most libraries don’t care, nor require, special fragment syntax, we can use the same callback name and work around like this:

const callback = (tagName, props, ...children) => {
// we called jsxFrag callback so ...
if (tagName === callback)
return children;

Awesome, all pieces seem to work well together, and jsx2tag module brings pretty much everything I’ve described in here to npm, plus more:

  • ?boolean attributes
  • .setter properties via bind(value)
  • @click like events, via onClick={...} handlers and similar
  • class vs function vs arrows invokes

You can see it working in CodePen via uhtml, uland, ube, or lit-html.

// uland via JSX ... goodbye kaboobie
/** @jsx h *//** @jsxFrag h */
import {createPragma} from '//unpkg.com/jsx2tag?module';
import {Component, render, html, useState} from '//unpkg.com/uland?module';
const h = createPragma(html);const Button = Component(({count}) => {
const [current, update] = useState(count);
return html`
<button onClick=${() => update(current + 1)}>
Clicked ${current} times
</button>
`;
});
render(document.body, <Button count={0} />);

Last Thoughts on JSX2TAG

While working with uland or ube this way is quite a joy, compared to their “no-tools-needed” native behavior, the current JSX transformer has some obvious drawback:

  • it was not not possible to understand which parts of the JSX was static and which part was meant as dynamic/interpolated. <div attr="name" class={value} /> would produce {attr:'name', class:value} as props, with no indication that attr="name" was static, hence it doesn’t need any diffing, or special logic around, it should always be the same and never bother the engine => JSX is slower. The “how much” is irrelevant with uhtml, thanks to its raw default performance, but if it’s extreme performance you are after, maybe don’t overuse it for every component, rather for major containers to compose on the page, and keep native performance in place.
  • the moment we use JSX, we kinda “defeat” the “no-tools-needed” nature of libraries behind, used to render DOM elements, without vDOM etc. The good news is that most online tools seem to offer JSX transformation out of the box, which is great for CodePen demos and similar, but if used in production we’ll inevitable need that transpilation step. If the DX is that better with JSX, it’s really up to us, but I’ve never missed it too much to date, although it’s been fun to find a way to use it even with my libraries.

--

--

Andrea Giammarchi

Web, Mobile, IoT, and all JS things since 00's. Formerly JS engineer at @nokia, @facebook, @twitter.