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"
}
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;
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',
}),
],
};
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)
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
Great! +1 for Solid.
Of course this is not a "production ready" project.
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.Yeah! This article is inspired by Ryan Carniato and what the Solid team had done with JSX.
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.
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
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 😅
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
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
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.
that's what i thought, I've already made changes to the jsx-runtime.js to make it work thank you