You don’t need React to use JSX

In this article, we explain the new JSX Transform, and how to use JSX without React.

Arnaud Dostes
6 min readSep 24, 2020

The official React blog does not get many updates, so a new post is always attention worthy, but this week’s post, Introducing the New JSX Transform, could be a much bigger deal than it seemingly appears.

In this post, Luna Ruan announces a new JSX Transform, and one of the benefits is that “you can use JSX without importing React.”.

For those who don’t know, JSX is the HTML like syntax which is used by React components to render in the browser:

import React from 'react';function MyComponent() {
return <div>This is JSX</div>;
}

When you use JSX, the compiler transforms it into React function calls that the browser can understand, so the above code becomes:

import React from 'react';function MyComponent() {
return React.createElement('div', null, 'This is JSX');
}

This is done using a babel plugin called @babel/plugin-transform-react-jsx

Now note the import React from 'react'; . That line is not inserted by the plugin, it is just copied over from the React component, and this is why React imports are needed in any file containing JSX. Even if there are no references to the React package in the original file, there are references to it in the transpiled result and that’s why React is needed.

But starting with v7.9.0, the JSX transform plugin provides a new mode, called automatic, which outputs this:

// Inserted by a compiler (don't import it yourself!)
import {jsx as _jsx} from 'react/jsx-runtime';

function MyComponent() {
return _jsx('div', { children: 'This is JSX' });
}

So this means that we no longer need to import React in files that use JSX, as the import is inserted by the compiler, so our component can now be written like this:

function MyComponent(){
return <div>This is JSX</div>;
}

That in itself is pretty convenient, but that’s not what blew my socks off. If we look a little deeper in the announcement, we find this note:

If you use JSX with a library other than React, you can use the importSource option to import from that library instead

So… That’s exactly what we’re going to do!

We’re going to write a file containing JSX, and write our own runtime to convert it from JSX to HTML, right there in a node application. JSX without React and without a browser!

First we’re going to initialize our project, and we’re going to need a few dependencies. Babel and the plugin-transform-react-jsx to compile our files, esm to support import/export statements, and of course jsdom to generate HTML in node. Notice how we are not importing React.

$ npm init -y
$ npm install @babel/cli @babel/core @babel/plugin-transform-react-jsx esm jsdom

To make sure all the versions are correct, here is my package.json

"dependencies": {
"@babel/cli": "^7.11.6",
"@babel/core": "^7.11.6",
"@babel/plugin-transform-react-jsx": "^7.10.4",
"esm": "^3.2.25",
"jsdom": "^16.4.0"
}

Next we need a .babelrc file which tells babel what to do. From the blog post, we know we need to do two things: use the new automatic runtime, and use the importSource option to specify our own runtime:

// .babelrc
{
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
"runtime": "automatic",
"importSource": "../runtime"
}
]
]
}

Let’s also make a few directories, one will contain our source code, one will contain the runtime we’re going to build, and one will contain the compiled source code:

$ mkdir src lib runtime

Our sample app is going to be a simple list of items:

// src/App.js
function List({ items }) {
return (
<ul>
{items.map((item, i) => (
<ListItem id={i}>
<Anchor value={item} />
</ListItem>
))}
</ul>
);
}
function ListItem({ children }) {
return <li>{children}</li>;
}
function Anchor({ value }) {
return <a href="#">{value}</a>;
}
function App() {
return <List items={[1, 2, 3, 4, 5]} />;
}
export default App;

And we’re also going to need an entry point which we can execute once the code is compiled. Just like a regular index.js in a React application, we are going to invoke a render function provided by our custom runtime, and that function takes two parameters, the top most component and a DOM node in which the app will be rendered. In a React application, that function would come from react-dom or react-native , here we’re going to write our own.

// src/index.jsimport { render } from "../runtime/jsx-runtime";import App from "./App";import { JSDOM } from "jsdom";// our jsdom document
const dom = new JSDOM(`<!DOCTYPE html><body><div id='root'/></body>`);
const { document } = dom.window;
const rootElement = document.getElementById("root");render(<App />, rootElement);console.log(document.body.innerHTML);

Let’s create two npm scripts, one to build the code and one to execute it.

"scripts": {
"build": "babel src -d lib",
"start": "node -r esm lib"
},

The build task compiles everything that is in src to output it in lib , and the start task runs the compiled code located in the lib folder.

Before writing the runtime, let’s build the code. As the name implies, we don’t need the runtime to build the code, only execute it. To compile the code, we use babel and the jsx transform plugin we configured in the .babelrc file

$ npm run build> babel src -d libSuccessfully compiled 2 files with Babel (239ms).

Let’s look at a snippet from the output file, it’ll tell us how the runtime is invoked:

// lib/App.js
import { jsx as _jsx } from "../runtime/jsx-runtime";
function List({
items
}) {
return _jsx("ul", {
children: items.map((item, i) => _jsx(ListItem, {
id: i,
children: _jsx(Anchor, {
value: item
})
}))
});
}
//...function App() {
return _jsx(List, {
items: [1, 2, 3, 4, 5]
});
}
export default App;

We see the path to the runtime being picked up from .babelrc and we see that a jsx function exported from a jsx-runtime module is expected by the runtime. It takes two parameters, a node that can be a string or another component (function), and props.

We’re going to write the runtime by heavily reusing the code written by Rodrigo Pombo in his article “Build your own React”.

// runtime/jsx-runtime.jsfunction jsx(type, config) {
if (typeof type === "function") {
return type(config);
}
const { children = [], ...props } = config;
const childrenProps = [].concat(children);
return {
type,
props: {
...props,
children: childrenProps.map((child) =>
typeof child === "object" ? child : createTextElement(child)
),
},
};
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
};
}

I’m not going to go into much details here, just know that we recursively execute each function until it resolves to a string (“il”, “ul”, etc…) that can be used to make an object used to build an HTMLElement.

And if we look at the compiled index.js, we see that our initial render call was transformed to this:

// lib/index.js
render(_jsx(App, {}), rootElement);

And that’s how we’re going to code our render function, again with few changes from Rodrigo Pombo ‘s article “Build your own React”. We know the render function receives the result of the jsx function we just coded.

// runtime/jsx-runtime.jsfunction render(element, container) {
const dom =
element.type === "TEXT_ELEMENT"
? container.ownerDocument.createTextNode("")
: container.ownerDocument.createElement(element.type);
const isProperty = (key) => key !== "children";
Object.keys(element.props)
.filter(isProperty)
.forEach((name) => {
dom[name] = element.props[name];
});
element.props.children.forEach((child) => render(child, dom));
container.appendChild(dom);
}
export { jsx, render };

Again, not going to go into too much details here, we recursively traverse the structure generated from the transpiled jsx code and convert each element to an HTMLElement using jsdom.

Now when we run the code, we’ll see this the result of the execution:

$ npm start> node -r esm lib<div id="root"><ul><li><a href="#">1</a></li><li><a href="#">2</a></li><li><a href="#">3</a></li><li><a href="#">4</a></li><li><a href="#">5</a></li></ul></div>

And that’s it!

Now to recap what we just did:

  • We wrote a sample app using JSX, and no other import (src/App.js ).
  • We configured babel to compile our app using the new automatic mode, and specified our own custom runtime.
  • We wrote a custom runtime to execute the transpiled code and output it to HTML in the console.

Why is this a big deal? It’s not that big a change after all, right?

Well it’s a big deal because it means JSX can be used without React. That was already true previously (Raphael Pombo does it in his article where he creates a React clone called Didact, and Preact also uses JSX), but now it’s made particularly easy, and this opens many doors. We could see JSX in other frameworks than React, and it also means JSX can be used to render other things than just HTML. By decoupling the runtime from the code, we can use the same JSX to achieve different goals just by using a different runtime. Previously, we were bound to whatever import was made in the component.

I’m very impatient to see what will come from this change in the weeks and months to come. Also noteworthy is that this was not made in a bubble, the people behind babel and React worked on this together, and the automatic mode will become the default option in Babel 8. The maintainers behind TypeScript, Create React App, Next.js, Gatsby, ESLint and Flow also pitched in and adopted the changes, and the RFC process was open to community feedback.

Thanks for reading, and let me know if you have any questions in the comments.

--

--