skip to content
Avatar Logo
JSX under the hood

What is JSX and how to build a jsx from the scratch?

/ 6 min read

Last Updated:

Disclaimer: The following content is intended for learning purposes. While inspired by React, the goal is to convey the fundamental concepts of building a library similar to React at a foundational level.

Hi guys,

JSX, or JavaScript XML, was brought to us by Facebook back in 2013 with React. Since then, many people have shared what JSX is and how to use it. Today, let’s dive into something fun, creating our custom JSX extension.

What is JSX?

Before we jump in, I hope you’re already familiar with using JSX in React. If not, no worries, here’s a quick explanation: JSX is a syntax extension that looks like HTML but is actually JavaScript and it looks like this:

import React from 'react'
function App() {
return <div>Hello world</div>
}
export default App

How does JSX work?

Browsers don’t understand JSX syntax right out of the box. We need a tool to turn our JSX code into plain JavaScript that browsers can run. This tool is called a transpiler, and one of the most popular ones is Babel.

Babel is a JavaScript compiler that converts JSX code into standard JavaScript that works with the React framework.

For example:

const element = <div>Hello JSX</div>

Gets transpiled into this JavaScript code:

import { jsx as _jsx } from 'react/jsx-runtime'
const element = _jsx('div', {
children: 'Hello JSX',
})

You can try it by yourself with Babel REPL!

After that, React will attach the resulting elements to the root element.

import { createRoot } from 'react-dom/client'
const domNode = document.getElementById('root')
const root = createRoot(domNode)
root.render(element)

This code renders our element to the DOM:

<div>Hello JSX</div>

Alright, now we have a basic understanding of how React renders the DOM.

But what’s really happening behind the scenes? To find out, let’s build our own custom JSX extension.

Let’s Build Our Own JSX Extension

We won’t be building a custom transpiler from scratch. Instead, we’ll use Babel. We’ll also use ParcelJs, a JavaScript compiler built on SWC. Parcel supports JSX right out of the box, and JSX is automatically enabled in .jsx or .tsx files.

First, we need to install the necessary dependencies:

Terminal window
npm install -D parcel @babel/core @babel/plugin-transform-react-jsx

Your package.json file should look like this:

package.json
{
"name": "JSX from the scratch",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "parcel ./src/index.html",
"build": "parcel build ./src/index.html"
},
"devDependencies": {
"parcel": "^2.0.0",
"@babel/core": "^7.24.7",
"@babel/plugin-transform-react-jsx": "^7.24.7"
}
}

Babel Configuration

Next, we need to set up Babel. Create a .babelrc file in the root of your project with the following settings:

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

There are two key options here: importSource and runtime

  • importSource allows us to set a custom module from which Babel will import the necessary functions (jsx, jsxs, Fragment) when using the automatic runtime. For example, with the above settings, Babel will do:
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from '/src/utils/jsx-runtime'
  • runtime decides which runtime to use. It can be classic or automatic. In short, classic requires manually imports JSX functions, while automatic handles the imports for you from the specified importSource.

Example with the classic option:

/** @jsx createElement */
import { createElement } from './utils/createElement'
const App = () => (
<div>
<h1>Hello, world!</h1>
</div>
)

As you can see, you have to manually import React.createElement or custom pragma functions import { createElement } from './utils/createElement' for each file.

Using the automatic runtime with a custom importSource allows Babel handle the imports automatically.

For more options, see here.

JSX Functions

Alright, let’s define the necessary functions for the automatic runtime:

./src/utils/jsx-runtime.js
export function createElement(type, props, ...children) {
return { type, props: { ...props, children } }
}
export function Fragment(props) {
return props.children
}
export function jsx(type, props, key) {
let children = props.children || []
if (!Array.isArray(children)) children = [children]
return createElement(type, { ...props, key }, ...children)
}
export function jsxs(type, props, key) {
return jsx(type, props, key)
}

Here’s a detailed explanation of each function:

  • createElement function creates a representation of a virtual DOM element, similar to React.createElement.
  • Fragment helps us group list of children without adding extra nodes.
  • jsx function is used by the JSX transpiler (Babel) when the runtime is set to automatic.
  • jsxs similar to jsx, but intended for elements with multiple children.

Next, we’ll need to add a function to convert a virtual DOM tree into actual DOM nodes and append them to a specified parent node.

./src/utils/render-jsx.mjs
import { Fragment } from './jsx-runtime.mjs'
export function renderJSX(element, parent) {
const stack = [{ element, parent }]
while (stack.length > 0) {
const { element, parent } = stack.pop()
if (typeof element === 'string' || typeof element === 'number') {
parent.appendChild(document.createTextNode(element))
continue
}
const { type, props = {} } = element
const domElement =
type === Fragment ? document.createDocumentFragment() : document.createElement(type)
Object.keys(props).forEach((name) => {
if (name === 'children') return
if (typeof props[name] !== 'undefined') domElement.setAttribute(name, props[name])
})
if (props.children) {
for (let i = props.children.length - 1; i >= 0; i--) {
stack.push({ element: props.children[i], parent: domElement })
}
}
parent.appendChild(domElement)
}
}

Render our Virtual DOM

Next, let’s add the ./src/App.mjs component:

./src/App.mjs
function App() {
return <div>Hello JSX</div>
}

And ./src/index.mjs to import everything and render the components:

./src/index.mjs
import { renderJSX } from './utils/render-jsx.mjs'
import App from './App'
renderJSX(App(), document.getElementById('app'))

Finally, let’s add ./src/index.html:

./src/index.html
<html>
<head>
<title>JSX from the scratch</title>
<meta charset="UTF-8" />
</head>
<body>
<div id="app" />
<script src="./index.mjs" type="module" />
</body>
</html>

And that’s all there is to it! Let’s give it a try. Run npm run start and open http://localhost:4321 in your browser. You’ll see the text Hello JSX displayed.

Conclusion

We’ve successfully built a simple JSX extension to render both complex and simple components. We’ve explored how JSX is parsed and utilized, and we’ve created functions to construct a tree structure, known as virtual DOM, then converted into an actual DOM tree for the browser to render.

However, at this stage, we don’t yet have the ability to dynamically re-render specific elements in the virtual DOM, manage state, or use hooks. This is just the beginning. Stay tuned for more in-depth coverage of these features in future articles.

You can find code here!!!

Thank you for reading 😊

And I go to write the next article.