DEV Community

Cover image for JSX Unchained: Make a template engine without React
Riccardo Tartaglia
Riccardo Tartaglia

Posted on

JSX Unchained: Make a template engine without React

Hello, developer.

Today, let's talk about JSX, the love-hate relationship of all React developers. Many hate it, many love it, but have you ever wondered how you could make the most of the power of JSX outside the usual React context?

Well, you're in the right place!

In the vast world of web development, the use of JSX in combination with React has changed how we create dynamic and responsive user interfaces. It must be admitted that the developer experience (DX) offered by React dominates over other frameworks, and JSX is one of the factors that has contributed to the success of this endeavor.

However, at times, you might feel the need to free JSX from this tight connection with React, thus opening the doors to new creative possibilities.

Let's peek behind the scenes.

Here's where the questions arise: how can we use JSX independently, without the weight of React? This article is here to guide you through the construction of a template engine that will allow you to do just that.

Imagine being able to leverage the powerful syntax of JSX in scenarios outside the traditional framework, customizing how JSX elements are evaluated and rendered (pretty cool, huh?).

Before diving into the creation of our template engine, let's take a quick look at what's behind the scenes between JSX and React:

JSX, which stands for JavaScript XML, is a kind of markup language that looks very similar to HTML. In reality, behind a JSX tag, there's the React.createElement function that handles the transformation of components. Yes, JSX tags are actually JavaScript functions (when you've recovered from the shock, keep reading).

Initially, JSX was designed to be used exclusively with React, but over time, it has evolved more and more to be considered a project on its own.

Our goal is, therefore, to create a template engine that adapts to our needs using JSX. Whether you're building a lightweight and fast app or exploring unusual scenarios, this template engine will be the key to opening new doors to our creativity.

Template Engine Design

Let's start with this:

I want to maintain the DX of React and write a core capable of translating my JavaScript functions, enriched with JSX, into a representation of pure HTML.

Imagine being able to define your user interface declaratively and then, with a magical touch, make it ready for action.

But how do we achieve all this?

Simple, we need a bundler! In particular, in this article, I'll use Webpack, but you can reconstruct everything with Vite, EsBuild, etc.

I'll try to guide you step by step.

Let's initialize the project.

Create a new folder and type:

npm init -y

Done? Great.

Now we need two things in particular:

The first thing is to install Webpack, and the second is to use a transpiler to "transform" our JSX into JavaScript objects.

As for the second point, we'll use Babel, but let's focus on Webpack for now.

npm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin

Great! Now let's install Babel.

npm i -D @babel/cli @babel/core @babel/preset-env babel-loader

Last piece: we need a Babel plugin that allows us to transpile JSX. We'll delve into its usage later.

npm i -D @babel/plugin-transform-react-jsx

Now that we've installed all the necessary dependencies, let's add the following scripts to our package.json.

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "start": "webpack serve --open",
    "dev": "webpack-dev-server --open"
}
Enter fullscreen mode Exit fullscreen mode

Plainly put, these scripts are for running our project and building it if we want to. Now, we need to create our "magic." Thanks to the babel/plugin-transform-react-jsx plugin, we can tell Babel to handle a custom runtime for transpiling JSX.

Essentially, we will now customize how Babel should "evaluate" JSX expressions. So, in the root of our project, let's create the file jsx-runtime.js.

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, ...rest } = props;
  if (typeof tag === 'function') return tag(props, children);
  const element = document.createElement(tag);

  for (const p in rest) {
    if (p.startsWith('on') && p.toLowerCase() in window) {
      element.addEventListener(p.toLowerCase().substring(2), props[p]);
    }
  }

  appendChild(element, children);
  return element;
};

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

This very minimal runtime allows us to transform our "components" with props into pure HTML, bidding farewell to the Virtual DOM. Consider that we could extend the runtime to handle custom components, directives, or anything else that comes to mind. In short, see it as the foundation of our template engine or a future framework.

Let's add the last piece of the puzzle: the webpack configuration file. Again, in the root of the project, let's create the webpack.config.js file.

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

const path = require('path');
module.exports = {
  entry: './src/index.js',
  mode: 'production',
  output: {
    path: `${__dirname}/dist`,
    filename: 'bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.(?:js|jsx|mjs|cjs)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [['@babel/preset-env']],
            plugins: [
              [
                '@babel/plugin-transform-react-jsx',
                {
                  runtime: 'automatic',
                  importSource: path.resolve(__dirname + '/./'),
                },
              ],
            ],
          },
        },
      },
    ],
  },
  resolve: {
    extensions: ['', '.js', '.jsx'],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

If we look at the module attribute, we find the configuration for Babel and the plugin plugin-transform-react-jsx, which refers to the root of the project to find our custom runtime file.

Aaaaaand that's it, we're done.

If you want a working example of what I've described so far, I've prepared a functional project for you on StackBlitz.

What's missing? A reactivity system. But that's another story...

Conclusion

And so, our journey into the discovery of an independent template engine based on JSX comes to an end. We've explored the reasons behind the need to free JSX from React, delved into the architecture of the template engine, and tackled the challenge of implementing a flexible and powerful templating language.

With this tool at your disposal, you're now armed with a new perspective in web development. Take a look at the benefits that this freedom can bring to your daily workflow.

Your feedback is valuable. If you've experimented with the proposed template engine, have questions, or suggestions on how to improve it, feel free to share them in the comments. If the article has been helpful, share it with your developer friends!

Top comments (11)

Collapse
 
raibtoffoletto profile image
Raí B. Toffoletto

Kudos! 🎉 This is very interesting to play with, certainly a good case study to understand better the inner works of babel + webpack!! Thanks for that 😁. But for a practical application I still would go with solid.js

Collapse
 
argonauta profile image
Riccardo Tartaglia

Great! +1 for Solid.
Of course this is not a "production ready" project.

Collapse
 
aquaductape profile image
Caleb Taylor

There's also solidjs, where JSX elements compile to real DOM elements, so console.log(<div>hi</div>) actually logs that div element instead of a function when called creates the element.

Collapse
 
argonauta profile image
Riccardo Tartaglia

Yeah! This article is inspired by Ryan Carniato and what the Solid team had done with JSX.

Collapse
 
harrybawsac profile image
Harry Bawsac

This is pretty funny! I’ll try it out, but I’m missing how to use it as part of something bigger. Thanks for sharing.

Collapse
 
argonauta profile image
Riccardo Tartaglia

This is a method to compose the "view" layer of your project, you can combine this system with any type of JS library for DOM manipulation or reactivity...Maybe you can try to make a new JS framework! :D

Collapse
 
harrybawsac profile image
Harry Bawsac

Just ran your example and works perfectly! Somehow I really like this idea.

Haha, making yet another JS framework... I don't think the world needs another one 😅

Thread Thread
 
rjjrbatarao profile image
rjjrbatarao • Edited

if you heared of alpinejs and htmx this it the most compatible one with this new approach :), and for the css we can use tailwind

Collapse
 
rjjrbatarao profile image
rjjrbatarao • Edited

omg amazing! i was looking out a way to integrate alpine.js and htmx with something like react but i dont need react because alpine and htmx has its own framework. one question though on how to add the attributes to jsx-runtime, sample works perfectly but attributes are lost

Collapse
 
argonauta profile image
Riccardo Tartaglia

The jsx-engine that I showed in the article is a minimal example.

You can start from here and write an engine that can preserve HTML attributes, or transform JSX props to HTML attributes.

Collapse
 
rjjrbatarao profile image
rjjrbatarao

that's what i thought, I've already made changes to the jsx-runtime.js to make it work thank you