JSX with Vanilla JavaScript
With just a small bit of configuration, you can incorporate JSX markup in your “vanilla” JavaScript project. This is thanks, in large part, to Vite’s use of Oxc.
Part 1: Minimal JSX Implementation
Section titled “Part 1: Minimal JSX Implementation”-
Create a bare-bones project using Vite.
Terminal window pnpm create vite@latest vanilla-jsx -- --template vanillacd vanilla-jsxpnpm install -
The stock template produces a page with a snazzy image at the top.
-
Create the Vite configuration file with the following contents. Since Vite uses Oxc as the JSX transpiler, we can inform Oxc that we want to use the “classic” runtime (instead of React) via a function we’ll call
h.vite.config.js import { defineConfig } from 'vite';export default defineConfig({oxc: {jsx: {runtime: 'classic',pragma: 'h',},},});We can call our function anything; the choice of
his just a traditional name based on the old hyperscript package. -
Create a small JavaScript function -
h- that we told Oxc to use during transpilation.src/h.js export function h(tag, props, ...children) {const element = document.createElement(tag);for (const [name, value] of Object.entries(props ?? {})) {if (name.startsWith('on') && typeof value === 'function') {element.addEventListener(name.slice(2).toLowerCase(),value);} else {element.setAttribute(name, value);}}element.append(...children.flat());return element;} -
In any JavaScript file where you want to write JSX, import
hand write your markup. For example, here’s a banner for announcements.src/announcement.jsx import { h } from './h';export function announcement(message) {return <div class="banner">{message}</div>;}; -
Use the function in your
main.js.main.js import './style.css'import heroImg from './assets/hero.png'import javascriptLogo from './assets/javascript.svg'import viteLogo from './assets/vite.svg'import { setupCounter } from './counter.js'import { announcement } from './announcement.jsx';document.body.prepend(announcement('We have evolved!')); -
Add a little styling to your CSS, and you’re good to go.
style.css #app {width: 1126px;max-width: 100%;margin: 0 auto;text-align: center;border-inline: 1px solid var(--border);min-height: 100svh;display: flex;flex-direction: column;box-sizing: border-box;}.banner {width: 1126px;max-width: 100%;margin: 0 auto;text-align: center;border-inline: 1px solid var(--border);display: flex;flex-direction: column;box-sizing: border-box;background-color: gold;font-weight: 500;} -
Here’s your final result.
Part 2: Adding Fragment Support
Section titled “Part 2: Adding Fragment Support”-
Edit the Vite configuration to tell Oxc to support fragments (
<></>).vite.config.js import { defineConfig } from 'vite';export default defineConfig({oxc: {jsx: {runtime: 'classic',pragma: 'h',pragmaFrag: 'Fragment',},},}); -
Provide our fragment handling.
src/h.js export const Fragment = Symbol('Fragment');export function h(tag, props, ...children) {if (tag === Fragment) {const fragment = document.createDocumentFragment();fragment.append(...children.flat());return fragment;}const element = document.createElement(tag);for (const [name, value] of Object.entries(props ?? {})) {if (name.startsWith('on') && typeof value === 'function') {element.addEventListener(name.slice(2).toLowerCase(),value);} else {element.setAttribute(name, value);}}element.append(...children.flat());return element;} -
Remember to import
Fragmentalongsideh, and you can start writing empty fragment tags as your “wrapper”.src/announcement.jsx import { h } from './h';import { h, Fragment } from './h';export function announcement(message) {return <div class="banner">{message}</div>return <><div class="banner">{message}</div><div class="banner">TADA! Fragments!</div></>}; -
If we want, we can tighten up our
h.jscode a little more.src/h.js export const Fragment = Symbol('Fragment');export function h(tag, props, ...children) {const node =tag === Fragment? document.createDocumentFragment(): document.createElement(tag);if (tag !== Fragment) {for (const [name, value] of Object.entries(props ?? {})) {if (name.startsWith('on') && typeof value === 'function') {node.addEventListener(name.slice(2).toLowerCase(), value);} else {node.setAttribute(name, value);}}}node.append(...children.flat());return node;}
At this point, we’ve got three clean JSX concepts working nicely in our “vanilla” JavaScript app.
// HTML elements<h1>Hello</h1>
// nesting<section> <h1>Hello</h1> <p>World</p></section>
// fragments<> <h1>Hello</h1> <p>World</p></>Part 3: Supporting Function Components
Section titled “Part 3: Supporting Function Components”While our announcement() function uses JSX internally, our main.js is still taking responsibility to call it as a function. React goes farther than what we’ve done so far by making Function Components possible. We’ll make a few more adaptations so that we can write <Announcement message='We have evolved!'></Announcement>.
-
Update your
h.jsto detect if thetagparameter is a function.src/h.js export const Fragment = Symbol('Fragment');export function h(tag, props, ...children) {if (typeof tag === 'function') {return tag(props ?? {}, children);}// the rest of the code stays the same... -
Modify your
announcement.jsxto the following.src/announcement.jsx import { h, Fragment } from './h';export function announcement(message) {export function Announcement(props) {return <><div class="banner">{message}</div><div class="banner">{props.message}</div><div class="banner">TADA! Fragments!</div></>};Notice how we’ve also changed our function name to be
Announcement. This naming convention is important because our JSX compiler needs to recognize that<Announcement>is not just a string. It also follows how React names its components. -
Let’s rename our
main.jstomain.jsx. Then, import thehandFragment. Now our component looks very React-like!main.jsx import './style.css'import heroImg from './assets/hero.png'import javascriptLogo from './assets/javascript.svg'import viteLogo from './assets/vite.svg'import { setupCounter } from './counter.js'import {h, Fragment} from './h.js';import { announcement } from './announcement.jsx';import { Announcement } from './announcement.jsx';document.body.prepend(announcement('We have evolved!'));document.body.prepend(<Announcement message='We have evolved!'></Announcement>);Notice that the file rename will break our project unless we also update our HTML.
index.html <body><div id="app"></div><script type="module" src="/src/main.js"></script><script type="module" src="/src/main.jsx"></script></body>
Conclusion
Section titled “Conclusion”Don’t be fooled. While this might look like a very attractive alternative to using React, we haven’t come anywhere near to all the capabilities React provides. So don’t expect this to be some sort of “drop-in” replacement. React is very powerful, and it’s worth your time to explore!