A lot of people don't know, but it's possible to run a React application in the browser using just one file.
You can even compile your entire React app directly in the browser. No need for server-side builds.
First, let's see how we can render React using pure HTML and JavaScript.
We'll use an import map to resolve React and ReactDOM, by pointing to their corresponding ESM URLs. It's a hack: esm.sh has already done the dependency bundling work.1
<!DOCTYPE html>
<html>
<head>
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/[email protected]",
"react-dom/client": "https://esm.sh/[email protected]/client"
}
}
</script>
</head>
<body>
<div id="root"></div>
<script type="module">
import React from 'react';
import { createRoot } from 'react-dom/client';
function App() {
const [count, setCount] = React.useState(0);
return React.createElement(
'div',
null,
React.createElement('h3', null, 'Count: ', count),
React.createElement(
'button',
{ onClick: () => setCount(count + 1) },
'Increment'
)
);
}
const root = createRoot(document.getElementById('root'));
root.render(React.createElement(App));
</script>
</body>
</html>If we place this code inside an iframe, a React application will be live and running. Try using Dev Tools to examine the network tab. You'll notice that React and ReactDOM are being loaded from ESM URLs:
This is awesome, but in practice nobody writes React by manually creating elements everywhere.
Instead, we typically write our components using JSX (or even better, TSX), but since browsers can't interpret JSX directly, we need to transform it into regular JavaScript first.
Transforming JSX with esbuild-wasm
One way to go from JSX to JavaScript is to use esbuild's esbuild.transform(), which can run in the browser using WebAssembly (WASM):2
import * as esbuild from 'esbuild-wasm';
await esbuild.initialize({
wasmURL: 'https://unpkg.com/[email protected]/esbuild.wasm',
});
const jsxCode = `
function App() {
return <h1>Hello, World!</h1>;
}
`;
const result = await esbuild.transform(jsxCode, {
loader: 'jsx',
target: 'es2015',
});Here's an interactive editor that uses esbuild.transform() to perform the transformation in real time. Edit the code and see the JavaScript output update in no time.
// Initializing esbuild...
esbuild.transform() works great for a single piece of code, but it doesn't support resolving multiple files, custom imports, or bundling.
Compiling Multiple Files and Bundling
The esbuild.build() method can do all of that:
- Resolves imports: Follows import statements and includes dependencies, allowing custom plugins to resolve and load.
- Bundles code: Combines multiple files into a single output.
- Tree-shakes: Removes unused code.
- Handles external modules: Can mark certain modules (like React) as external and load them separately.
Here's how to set up esbuild.build() to handle path aliases and bundle local files:
// Define the virtual file system
const virtualFiles = {
"@/entry": {
contents: `...`,
loader: "tsx",
},
"@/components/ui/button": {
contents: `...`,
loader: "tsx",
},
"@/lib/utils": {
contents: `...`,
loader: "js",
},
};
const result = await esbuild.build({
entryPoints: ["@/entry"],
bundle: true,
format: "esm",
write: false,
plugins: [
{
name: "virtual-files",
setup(build) {
// First, a resolver to mark React as external and resolve @/ paths to "virtual" namespace
build.onResolve({ filter: /.*/ }, (args) => {
// Mark React as external (loaded via importmap)
if (/^(react|react-dom|react-dom/client)$/.test(args.path)) {
return { path: args.path, external: true };
}
// Resolve all @/ paths to virtual namespace
if (args.path.startsWith("@/")) {
return {
path: args.path,
namespace: "virtual",
};
}
});
// Handles the "virtual" namespace to return file contents
build.onLoad({ filter: /.*/, namespace: "virtual" }, (args) => {
return virtualFiles[args.path] || null;
});
},
},
],
});
const bundledCode = result.outputFiles?.[0]?.text;For more details on esbuild's build options, see the official esbuild documentation.
Below is an editor powered by esbuild.build(). The code is compiled and bundled in the browser, and then rendered in an iframe.
Wrapping Up
Compiling React in the browser opens up exciting possibilities. You can build your own playground, a lightweight vibe-coding tool, or showcase animations alongside a code editor so readers can tweak examples. All in the user's browser!
As an example, check out this interactive React demo built with Motion, where you can modify and experiment with it live.
The same technology is behind faces.app, a platform that that lets users generate software artifacts for storytelling. Give it a try!