Skip to Content

Origins of JSX and Why It Exists

As if plain JavaScript wasn't complex enough already…

Published on

Random characters surrounding an HTML tag that transforms into a function call.

Often, JSX is described as allowing you to write HTML inside JavaScript. That might not make sense for you at first, and rightfully so — both languages serve different purposes and merging them might seem confusing.

But JSX is not a mix of HTML and JavaScript. It's just plain JavaScript with some clever syntax tricks that make it easier to represent HTML. It's similar to how Sass(opens in new tab) brings a lot of productive features but always boils down to regular CSS.

You're going to see how JSX came to be by understanding the problem it solves and by building your very own JSX interpreter!

The Problem

Let's say you have to create this HTML tree using JavaScript:

<div id="content">
	<h1 class="large">Hello World!</h1>
</div>

First of all, why would you want to use JavaScript for that? In this particular case, there's no reason, but if you're building a highly dynamic SPA (single-page application)(opens in new tab), for example, and you use a framework or library like React(opens in new tab), most HTML will have to be created through JavaScript.

So, creating those elements would probably look like this:

let div = document.createElement("div");
div.id = "content";

let h1 = document.createElement("h1");
h1.classList.add("large");
h1.innerHTML = "Hello World!";

div.appendChild(h1);

But now let's say you have to create the following HTML instead:

<div id="content">
	<img src="https://placehold.co/600x400" alt="sample image">

	<div class="text">
		<h1 class="large">
			<a href="https://example.com/" target="_blank">
				Click here!
			</a>
		</h1>

		<p>
			Lorem ipsum dolor sit amet...
		</p>
	</div>
</div>

There's a lot more going on here and creating all those elements in that manner would be too annoying and time-consuming.

Notice that there's a pattern, however — we write an element's tag name, some attributes, and then its children. For those children, we do the same. We could write ourselves a function that does exactly that:

function element(tagName, attributes, ...children) {
	const el = document.createElement(tagName);

	if (attributes) {
		for (const attrName in attributes) {
			const attrValue = attributes[attrName];
			el[attrName] = attrValue;
		}
	}

	children.forEach((child) => {
		if (typeof child === "string") {
			el.appendChild(document.createTextNode(child));
		} else {
			el.appendChild(child);
		}
	});

	return el;
}

With this element function, we can create elements much more concisely with just a few function calls:

const text = element("p", null, "text");
const child = element("div", { id: "child" }, text);
const parent = element("div", { id: "content" }, child);
document.body.appendChild(parent);

As you can see, using the function is simple — we specify the tag name as the first argument, then all attributes as an object, and finally pass any child elements as the remaining arguments.

The code above would result in the following DOM(opens in new tab) tree:

<div id="content">
	<div id="child">
		<p>text</p>
	</div>
</div>

Notice that we can make this even more concise by avoiding the use of variables and simply invoking the function in place:

document.body.appendChild(
	element("div", { id: "content" },
		element("div", { id: "child" },
			element("p", null, "text")
		)
	)
);

This is starting to look a lot like HTML code, doesn't it? Well, Dominic Tarr(opens in new tab) thought so as well over 10 years ago and created a project called HyperScript(opens in new tab) which did exactly that.

Using HyperScript, you could create the same DOM tree like so:

const h = require("hyperscript");
document.body.appendChild(
	h("div", { id: "content" },
		h("div", { id: "child" },
			h("p", null, "text")
		)
	)
);

However, that syntax is kind of hard to write and perhaps even harder to read by other people.

The Solution

This is where JSX comes in. It allows you to jump from writing code that almost looks like HTML to code that pretty much is HTML. Later, that pseudo-HTML code is transpiled(opens in new tab) down to the regular JavaScript that browsers understand.

Take the following JSX, for example:

const el = (
	<div id="content">
		<div id="child">
			<p>text</p>
		</div>
	</div>
);

We can use Babel(opens in new tab), or TypeScript(opens in new tab) and its tsc compiler(opens in new tab), to transpile it down to regular JavaScript. The result is almost identical to what we had earlier with the HyperScript library:

const el = (React.createElement("div", { id: "content" },
	React.createElement("div", { id: "child" },
		React.createElement("p", null, "text"))));

The only difference is that instead of h, we have React.createElement, which is called a JSX factory function.

As the name implies, the job of the factory function is to create elements with the specified tag names, attributes, and children. Then, the action of invoking that function to create those elements and form the desired DOM tree is called rendering.

Why React?

You might be thinking "why did React(opens in new tab) suddenly appear and what does it have to do with all of that?"

As you might already know, React is created and maintained by Meta (Facebook). Well, JSX was also invented by the Facebook team and is often used alongside React, although both are two separate tools with different goals:

  • JSX is only concerned with syntax — offering an easier way to represent HTML structures in JavaScript which results in a bunch of calls to some factory function that performs rendering and brings that HTML structure to life

  • React is only concerned with providing that previously mentioned factory function — the thing that actually makes your application work by creating, updating, and deleting elements reactively

You can use React without JSX and you can use JSX without React. In fact, many other frameworks, like Preact(opens in new tab) and Vue(opens in new tab), use or support JSX as well. They simply provide their own different factory functions for it.

For example, the TypeScript compiler provides the jsxFactory setting(opens in new tab) which allows you to change React.createElement to something else, like h, which is used by Preact and Vue.

If you wonder "why h" — well, it's exactly because of that old HyperScript library. It seems that the creators of these newer frameworks have decided to follow that same convention.

With jsxFactory set up, you could write something like this:

import { h } from "preact";
const el = <p>text</p>;

…and, because jsxFactory is set to "h", it would get transpiled to the following valid JavaScript:

import { h } from "preact";
const el = h("p", null, "text");

You can experiment with this in the TypeScript playground(opens in new tab) to see how JSX is getting transpiled to JavaScript.

Custom JSX Factory

Sure, we can import that h function from Preact or rely on React.createElement, but we can also use our very own element function that we created earlier!

All we have to do is to simply transpile our JSX with the jsxFactory option set to element:

tsc element.jsx --allowJs --jsx react --jsxFactory element

That would turn the following JSX:

function element(tagName, attributes, ...children) {
	const el = document.createElement(tagName);

	if (attributes) {
		for (const attrName in attributes) {
			const attrValue = attributes[attrName];
			el[attrName] = attrValue;
		}
	}

	children.forEach((child) => {
		if (typeof child === "string") {
			el.appendChild(document.createTextNode(child));
		} else {
			el.appendChild(child);
		}
	});

	return el;
}

const el = (
	<div id="content">
		<div id="child">
			<p>text</p>
		</div>
	</div>
);

…into the following JavaScript that we could run in the browser:

function element(tagName, attributes, ...children) {
    const el = document.createElement(tagName);
    if (attributes) {
        for (const attrName in attributes) {
            const attrValue = attributes[attrName];
            el[attrName] = attrValue;
        }
    }
    children.forEach((child) => {
        if (typeof child === "string") {
            el.appendChild(document.createTextNode(child));
        }
        else {
            el.appendChild(child);
        }
    });
    return el;
}
const el = (element("div", { id: "content" },
    element("div", { id: "child" },
        element("p", null, "text"))));

Conclusion

Creating DOM elements explicitly by hand is cumbersome, which has inspired the creation of HyperScript. However, writing numerous JavaScript functions is not manageable either, so JSX was invented to make it syntactically easier.

But when writing JSX, you don't really write actual HTML inside JavaScript. You're merely using the HTML syntax as a more developer-friendly way of representing the DOM tree that you want your favorite framework to build up for you.

After getting transpiled, JSX boils down to just a bunch of function calls to a predefined factory function that determines how to render your desired HTML structure.