DEV Community

Cover image for Vanilla JSX
Max Korsunov
Max Korsunov

Posted on

Vanilla JSX

Hi everyone! In this article I will tell how you can use JSX in a project with TypeScript and Vite bundler. This material is for you, if you:

  • ⚛️ Have experience with React, but no idea about how it handles JSX
  • 🕵️‍♂️ Curious about front-end fundamentals
  • 🤓 A geek who loves vanilla TypeScript and all around it

Why? For fun, of course! The idea of vanilla JSX is not suitable for a real project without over-engineering. Most likely, scaling the JSX support would cause the creation of the new frontend framework. So, open the GitHub repo in a new browser tab and make yourself comfortable. We have a deep dive into the JSX ahead!

What is JSX?

JSX is a syntactic extension over JS. It is not in the ECMAScript standards, so the tools like Babel and React deal with JSX transpiling into the plain JavaScript. Let's look at the classic JSX example:

const profile = (
  <div>
    <img src="avatar.png" className="profile" />
    <h3>{[user.firstName, user.lastName].join(" ")}</h3>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

After the @babel/plugin-transform-react-jsx run, the code will become understandable by browsers:

const profile = React.createElement(
  "div",
  null,
  React.createElement("img", { src: "avatar.png", className: "profile" }),
  React.createElement("h3", null, [user.firstName, user.lastName].join(" "))
);
Enter fullscreen mode Exit fullscreen mode

As you can see, Babel successfully transformed JSX into a neat React.createElement function. It comprises the wrapper tag, its properties (or attributes, in the above case - null) and child elements, which, in their turn, are created with the same function.

React, Vue, and Solid frameworks deal with JSX by themselves, but they do it differently. This happens because they have different implementations of the createElement function. By the way, it is called JSX Pragma. When I learned about it, I immediately decided to create my own Pragma.

JSX Parsing

Before jumping into the Pragma creation, we need to learn how to parse JSX. For small modern project without old browsers support, we wouldn't need Babel, but Vite or TypeScript is enough. We will use both.

JSX Code transformation: from code to typescript parser, vite bundler, into the Vanilla JS code

Vite is a modern frontend app bundler. Compared to Webpack, it serves the source code over native ESM. To bootstrap the project, we just need to run the command:

npm create vite@latest
Enter fullscreen mode Exit fullscreen mode

Vite and TypeScript, by default, parse JSX in .jsx or .tsx files. It substitues the result of parsing into the React.createElement function. Though, if we want a custom function to be substituted, we need to change the tsconfig.json.

{
  "compilerOptions": {
    "jsx": "preserve",
    "jsxFactory": "h",
    "jsxFragmentFactory": "Fragment"
  }
}
Enter fullscreen mode Exit fullscreen mode

If you are writing the app without TypeScript, change the vite.config.js.

import { defineConfig } from 'vite';
export default defineConfig({
  esbuild: {
    jsxFactory: 'h',
    jsxFragment: 'Fragment'
  }
});
Enter fullscreen mode Exit fullscreen mode

These settings tell the parser to use the h (meaning hyperscript, hypertext + javascript) function for JSX with only one root element and Fragment for multiple-root JSX.

JSX Pragma

After configuring the parser to handle the h function, we can start implementing it in src/pragma.ts.

// Tag can be string or a function if we parse the functional component 
type Tag = string | ((props: any, children: any[]) => JSX.Element);

// Attributes of the element – object or null
type Props = Record<string, string> | null;

// Element children – return value from the h()
type Children = (Node | string)[];

export const h = (tag: Tag, props: Props, ...children: Children) => {
  // If tag is a component, call it
  if (typeof tag === 'function') {
    return tag({ ... props }, children);
  }
  // Create HTML-element with given attributes
  const el = document.createElement(tag);
  if (props) {
    Object.entries(props).forEach(([key, val]) => {
      if (key === 'className') {
        el.classList.add(...(val as string || '').trim().split(' '));
        return;
      }
      el.setAttribute(key, val);
    });
  }

  // Append child elements into the parent
  children.forEach((child) => {
    el.append(child);
  });

  return el;
};
Enter fullscreen mode Exit fullscreen mode

Just like createElement, the h function accepts the tag name (or a functional component), properties and results of the h function over the child elements.

All .jsx files must import the h function, so it is in the code scope after the transpilation. For example, this is a simple JSX usage.

import { h } from '../pragma';
import { LikeComponent } from './like';
export const App = (
  <main className="hello">
    <h1>
      Hello JSX!
    </h1>
    <LikeComponent big />
  </main>
);
Enter fullscreen mode Exit fullscreen mode

All we have to do now is add the transpiled code into the HTML:

import { App } from './components/app';
const app = document.querySelector<HTMLDivElement>('#app')!
app.append(App);
Enter fullscreen mode Exit fullscreen mode

That's it! We've created the app in with TypeScript that parses the JSX, and Pragma that creates the correct DOM for displaying it on a website!

Practical use

As I said in the beginning, this idea is not meant for use in real projects. It only shows how easy it is to parse JSX without the use of runtime libraries, literally working in vanilla JS.

The concept of JSX Pragma is difficult to scale. If you wish to add logic to functional components, you'd have to handle the cases with variables and event listeners, thus re-implementing the concept of reactivity.

Conclusion

It turned out that such a non-standard concept as JSX Pragma can easily be handled with no frameworks, with vanilla JS only! 

I encourage you to experiment with all the technologies that you can get your hands on, go as deep as possible into them. Good luck!

Top comments (0)