DEV Community

Cover image for How to create JSX template engine from scratch
Rahul Sharma
Rahul Sharma

Posted on • Updated on

How to create JSX template engine from scratch

What is JSX and How Does it Work?

JSX is an XML-like syntax extension to JavaScript. It is similar to a template language, but it has the full power of JavaScript. JSX gets compiled to React.createElement() calls which return plain JavaScript objects called “React elements”. To get a basic introduction to JSX see the docs here.

In this article, we will take a look at how JSX works and how to create custom components using JSX without using any frameworks.

Let's get started

Create a new Project

Create a directory for your project by running the following command in your terminal:

  mkdir jsx-demo
  cd jsx-demo
  npm init -y
Enter fullscreen mode Exit fullscreen mode

Install Babel

Babel is a JavaScript compiler. It converts ECMAScript code into a backward-compatible version of JavaScript in current and older browsers or environments, Babel is used to compiling JSX to JavaScript.

  npm install --save-dev @babel/core @babel/cli @babel/plugin-transform-react-jsx
Enter fullscreen mode Exit fullscreen mode

Create a babel.config.js file

const presets = [];
const plugins = [
  [
    "@babel/plugin-transform-react-jsx",
    { runtime: "automatic", importSource: "./core" },
  ],
];

module.exports = {
  presets,
  plugins,
};
Enter fullscreen mode Exit fullscreen mode

NOTE: The importSource option is used to specify the path to the module that will be used to create the JSX elements. In this case, we are using the core module. We will create this module in the next step.


Create a src directory in your project, This is where we will write our code.

  mkdir src
  cd src
  touch core/jsx-runtime.js
Enter fullscreen mode Exit fullscreen mode

Let's create an index.html file in the root directory of our project

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My App</title>
  </head>
  <body>
    <h1>Hello Friends</h1>
    <div id="root"></div>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

NOTE: The id of the root element is important. It is used to mount the application. We will see how to do this in the next step.


Let's create App.jsx and Button.jsx

We will create a App.jsx and Button.jsx files in the src directory. App.jsx will be the entry point for our project.

Button.jsx

const Button = ({ children, onClick }) => (
  <button onClick={onClick}>{children}</button>
);
export default Button;
Enter fullscreen mode Exit fullscreen mode

App.jsx

import Button from "./Button";

const App = () => (
  <div>
    <Button onClick={() => alert(1)}>Click 11</Button>
    <Button onClick={() => alert(2)}>Click 12</Button>
    <Button onClick={() => alert(3)}>Click 13</Button>
  </div>
);

const rootElement = document.getElementById("root");
rootElement.appendChild(<App />);

export default App;
Enter fullscreen mode Exit fullscreen mode

Let's add the babel script in package.json

This script will compile our JSX code to JavaScript code. The output will be in the dist directory.

"scripts": {
    "build-babel": "babel src -d dist",
},
Enter fullscreen mode Exit fullscreen mode

Let's compile our code

    npm run build-babel
Enter fullscreen mode Exit fullscreen mode

Let's see how it looks like.

import Button from "./Button";
import { jsx as _jsx } from "./core/jsx-runtime";
import { jsxs as _jsxs } from "./core/jsx-runtime";

const App = () =>
  _jsxs("div", {
    children: [
      _jsx(Button, {
        onClick: () => alert(1),
        children: "Click 11",
      }),
      _jsx(Button, {
        onClick: () => alert(2),
        children: "Click 12",
      }),
      _jsx(Button, {
        onClick: () => alert(3),
        children: "Click 13",
      }),
    ],
  });

const rootElement = document.getElementById("root");
rootElement.appendChild(_jsx(App, {}));
export default App;
Enter fullscreen mode Exit fullscreen mode

NOTE:

  1. The jsx function is imported from the core module. We added the babel.config.js file, but we have not created jsx-runtime yet. We will create it in the next step.
  2. JSX code is converted to JavaScript function, Which will call the jsx function.

Most important part of this article is creating the jsx-runtime module. This is where the magic happens.

All the components import the jsx function from the jsx-runtime module. The jsx function is responsible for creating the elements.

const add = (parent, child) => {
  parent.appendChild(child?.nodeType ? child : document.createTextNode(child));
};

const appendChild = (parent, child) => {
  if (Array.isArray(child)) {
    child.forEach((nestedChild) => appendChild(parent, nestedChild));
  } else {
    add(parent, child);
  }
};

export const jsx = (tag, props) => {
  const { children } = props;
  if (typeof tag === "function") return tag(props, children);
  const element = document.createElement(tag);
  Object.entries(props || {}).forEach(([name, value]) => {
    if (name.startsWith("on") && name.toLowerCase() in window) {
      element.addEventListener(name.toLowerCase().substr(2), value);
    } else {
      element.setAttribute(name, value);
    }
  });
  appendChild(element, children);
  return element;
};

export const jsxs = jsx;
Enter fullscreen mode Exit fullscreen mode

Let's understand how the jsx function works.

As you can see, the jsx function takes two arguments, the first argument is the tag name and the second argument is the props. The jsx function returns a plain JavaScript object. If the tag is a function, it will call the function and return the result. If the tag is a string, it will create a DOM element, set all the props to the element, and return the element.


The last step creates a bundle file using webpack And injects the bundle file into the index.html file.

Install webpack, webpack-cli and html-webpack-plugin using the following command:

    npm install --save-dev webpack webpack-cli html-webpack-plugin
Enter fullscreen mode Exit fullscreen mode

NOTE: We can also configure babel in the webpack config, But I'm using babel separately to get a better understanding of how JSX code is compiled.


Create a webpack.config.js file

Add the following code to the webpack.config.js file.

const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./dist/App.js",
  mode: "production",
  output: {
    path: `${__dirname}/build`,
    filename: "bundle.js",
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "src/index.html",
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

NOTE: The entry is the path of the compiled file. The output is the path where the bundle file will be created. The template is the path of the index.html file.


Update package.json scripts

"scripts": {
  "start": "webpack serve --open",
  "build-babel": "babel src -d dist",
  "build-webpack": "webpack --config webpack.config.js",
  "build": "npm run build-babel && npm run build-webpack"
},
Enter fullscreen mode Exit fullscreen mode

NOTE: The start script will start the webpack dev server. The build script will compile the code using babel and create the bundle file using webpack.


Time to build our project

    npm run build
Enter fullscreen mode Exit fullscreen mode

After running the above command, you will see a build directory in your project. This is where the compiled code will be saved. Also, the dist directory will have the compiled code. We can delete the dist directory if we want, I would recommend keeping it for reference.


Run the project

    npm run start
Enter fullscreen mode Exit fullscreen mode

Quick Tip: You can use serve package to start the HTTP server from any directory. This is very useful when you are working on a static website.

    cd build
    npx serve
Enter fullscreen mode Exit fullscreen mode

Live Demo: Here

In the next article, We will also add a state to this app.

Thank you for reading 😊

Got any additional questions? please leave a comment.


Must Read If you haven't

More content at Dev.to.
Catch me on

Youtube Github LinkedIn Medium Stackblitz Hashnode HackerNoon

Top comments (4)

Collapse
 
melone14 profile image
melone14

Hey, great article! Thank you. Can I find somewhere an article with adding state to this app?

Collapse
 
devsmitra profile image
Rahul Sharma

@melone14 you can refer to this is not the state implementation but something similar.

stackblitz.com/edit/reactive-frame...

Collapse
 
nflyr profile image
Luis Filipe Ferreira

Hey, great article. Exactly what I was looking for. Thank you.

Collapse
 
hanss profile image
Hans Schenker

Here is a Typescript version of your jsx function:

const add = (parent: HTMLElement,
child: HTMLElement
| string
| number
| boolean
| null
| undefined) => {

parent.appendChild(child?.nodeType ? child : document.createTextNode(String(child)));
};

const appendChild = (parent: HTMLElement,
child: HTMLElement
| string
| number
| boolean
| null

| undefined) => {

if (Array.isArray(child)) {
child.forEach((nestedChild) => appendChild(parent, nestedChild));
} else {
add(parent, child);
}
};

export const jsx = (tag: string | Function,
props: { [key: string]: any }) => {

const { children, ...rest } = props;

if (typeof tag === "function") return tag({ ...rest, children });

const element = document.createElement(tag);

Object.entries(rest || {}).forEach(([name, value]) => {
if (name.startsWith("on") && name.toLowerCase() in window) {
element.addEventListener(name.toLowerCase().substr(2), value);
} else {
element.setAttribute(name, String(value));
}
});

appendChild(element, children);

return element;
};

export const jsxs = jsx;