
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.
- Part 1 - JSX and virtual DOM
- Part 2 - Custom state management
- Part 3 - VirtualDOM: Optimize rendering process
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:
npm install -D parcel @babel/core @babel/plugin-transform-react-jsx
Your package.json
file should look like this:
{ "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:
{ "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
orautomatic
. In short,classic
requires manually imports JSX functions, whileautomatic
handles the imports for you from the specifiedimportSource
.
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:
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 tojsx
, 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.
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:
function App() { return <div>Hello JSX</div>}
And ./src/index.mjs
to import everything and render the components:
import { renderJSX } from './utils/render-jsx.mjs'
import App from './App'
renderJSX(App(), document.getElementById('app'))
Finally, let’s add ./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>