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>
);
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(" "))
);
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.
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
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"
}
}
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'
}
});
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;
};
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>
);
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);
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)