React & JSX
React works out of the box. Vantris understands JSX natively and uses the automatic JSX runtime by default, so you don't need to import React. For stateful Fast Refresh — edits that update a component in place without losing its state — add the official @vantris/react plugin.
Zero-config JSX
Install the runtime packages and point your entry at a .tsx file:
npm install react react-dom// src/main.tsx
import { createRoot } from "react-dom/client";
function App() {
return <h1>Hello Vantris</h1>;
}
createRoot(document.getElementById("app")!).render(<App />);<!-- index.html -->
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>That's a complete React app — no Babel config, no import React from "react".
Choosing the JSX runtime
The runtime is selected by your tsconfig.json:
tsconfig jsx | Runtime | import React? |
|---|---|---|
"react-jsx" | automatic | not needed |
"react" | classic | required |
For non-React JSX libraries (Preact, Emotion), set jsxImportSource:
// tsconfig.json — Preact
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
}
}Fast Refresh with @vantris/react
Native JSX gets you rendering, but a plain edit re-runs the whole module and resets component state. The @vantris/react plugin adds React Fast Refresh: a save updates the component in place and keeps its state (form inputs, counters, open/closed panels).
npm install -D @vantris/react// vantris.config.ts
import { defineConfig } from "vantris";
export default defineConfig({
plugins: ["@vantris/react"],
});That's it. vantris dev now has Fast Refresh; vantris build compiles JSX as usual (the plugin steps aside in a build).
Plugins are referenced by string
Vantris loads plugins by string — a package name or a local path. To pass options, wrap the plugin in a tiny local module and reference that file instead. See With options.
What the plugin adds
- Fast Refresh — component edits update in place, preserving state, over Vantris's HMR channel (dev only).
- JSX via Babel — compiled with the automatic runtime by default; switch to
classicor a customjsxImportSourcethrough the plugin's options. - Pre-bundling — seeds
optimizeDeps.includewithreact,react-dom,react-dom/client, and the JSX runtimes, so the dev server is warm before the first request.
In a production build the plugin defers JSX compilation to Vantris/Rolldown and does nothing else.
The full option table, the "with options" wrapper pattern, and how each hook works are on the @vantris/react page.
Using HMR in a React app
With @vantris/react installed, individual components refresh automatically — you rarely touch import.meta.hot directly. When you need custom teardown for non-component modules (stores, singletons), the HMR API is still available and behaves the same as in any Vantris app.
Type declarations
Install the React types for editor support and your typecheck script:
npm install -D @types/react @types/react-domAnd add vantris/client to tsconfig.json types for import.meta.env, import.meta.hot, and asset imports. See TypeScript and Client Types.
A complete minimal example
// package.json
{
"type": "module",
"scripts": {
"dev": "vantris dev",
"build": "vantris build",
"preview": "vantris preview"
},
"dependencies": { "react": "^19", "react-dom": "^19" },
"devDependencies": {
"vantris": "^1.2.0",
"@vantris/react": "^0.1.0",
"@types/react": "^19",
"@types/react-dom": "^19"
}
}// vantris.config.ts
import { defineConfig } from "vantris";
export default defineConfig({ plugins: ["@vantris/react"] });// src/main.tsx
import { createRoot } from "react-dom/client";
import { App } from "./App";
createRoot(document.getElementById("app")!).render(<App />);// src/App.tsx — edit this while the counter is non-zero; the count survives
import { useState } from "react";
export function App() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>Count: {n}</button>;
}