userland.run70Try the demo
Example · react-studio

A Vite-built React 19 app, served from NanoVM

node · host JS engine

A small interactive React 19 app — a counter, a Fibonacci memo, a list — built with Vite. npm run demos:build installs the pinned toolchain with npm ci and runs vite build; the output is index.html plus one JS file with every asset inlined.

The build happens ahead of time, on the developer machine or in CI. In the browser, a dependency-free Node.js static server (server.js) serves dist/ from NanoVM's filesystem into the preview iframe, where React mounts and runs as usual.

Run it

dev server

Serve the React build

Seeds the built dist/ into /work, then runs this static server. The app in the preview is live — click its buttons.

javascript
// A dependency-free static file server for the pre-built Vite dist/. Runs inside
// NanoVM; serves the React app to the preview iframe via the service-worker bridge.
const http = require("http");
const fs = require("fs");
const path = require("path");

// dist/ relative to the working directory (`cd <demo> && node server.js`).
const ROOT = path.resolve("dist");
const TYPES = {
  ".html": "text/html; charset=utf-8",
  ".js": "text/javascript; charset=utf-8",
  ".css": "text/css; charset=utf-8",
  ".json": "application/json",
  ".svg": "image/svg+xml",
  ".map": "application/json",
};

const server = http.createServer(function (req, res) {
  let url = req.url.split("?")[0];
  if (url === "/" || url === "") url = "/index.html";
  const file = path.join(ROOT, url);
  fs.readFile(file, function (err, data) {
    if (err) {
      // SPA fallback → index.html
      fs.readFile(path.join(ROOT, "index.html"), function (e2, html) {
        if (e2) {
          res.writeHead(404);
          res.end("not found");
          return;
        }
        res.writeHead(200, { "Content-Type": TYPES[".html"] });
        res.end(html);
      });
      return;
    }
    res.writeHead(200, { "Content-Type": TYPES[path.extname(file)] || "application/octet-stream" });
    res.end(data);
  });
});

server.listen(3000, function () {
  console.log("react-studio listening on 3000");
});
loading…
Expected output
react-studio

How it runs

  1. The files of dist/ are fetched from /demo-files/react-studio/dist/ and written to /work/dist/.
  2. server.js starts with nano.node() on the host JS engine and serves dist/ on port 3000.
  3. The service-worker bridge routes the iframe to it; Vite's base: "./" keeps asset URLs relative to the preview path.

Source

From demos/react-studio/.

src/App.jsx
jsx
import { useEffect, useMemo, useState } from "react";

const C = {
  bg: "#0e0e10",
  panel: "#16161c",
  border: "#2a2a31",
  text: "#cfcfda",
  dim: "#8c8c97",
  cyan: "#5fd6d1",
  violet: "#a984f5",
  amber: "#e6b45a",
};

function Stat({ label, value, color }) {
  return (
    <div style={{ background: C.panel, border: `1px solid ${C.border}`, borderRadius: 10, padding: "12px 14px" }}>
      <div style={{ fontSize: 10, letterSpacing: ".12em", textTransform: "uppercase", color: C.dim }}>{label}</div>
      <div style={{ fontSize: 20, marginTop: 4, color: color || C.text, fontVariantNumeric: "tabular-nums" }}>{value}</div>
    </div>
  );
}

export default function App() {
  const [count, setCount] = useState(0);
  const [tick, setTick] = useState(0);
  const [items, setItems] = useState(["render", "hydrate", "reconcile"]);

  useEffect(() => {
    const id = setInterval(() => setTick((t) => t + 1), 1000);
    return () => clearInterval(id);
  }, []);

  const fib = useMemo(() => {
    let a = 0, b = 1;
    for (let i = 0; i < count; i++) [a, b] = [b, a + b];
    return a;
  }, [count]);

  return (
    <div
      style={{
        minHeight: "100vh",
        margin: 0,
        background: C.bg,
        color: C.text,
        fontFamily: "ui-monospace, monospace",
        padding: "28px 22px",
      }}
    >
      <div style={{ maxWidth: 560, margin: "0 auto" }}>
        <div style={{ display: "flex", alignItems: "baseline", gap: 10 }}>
          <h1 style={{ margin: 0, fontSize: 19, color: C.violet }}>react-studio</h1>
          <span style={{ fontSize: 11, color: C.dim }}>vite build · served from NanoVM</span>
        </div>
        <p style={{ fontSize: 12, color: C.dim, marginTop: 6 }}>
          A React 19 app, pre-built with Vite, served by a Node.js http server running inside NanoVM in this tab.
        </p>

        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 10, margin: "16px 0" }}>
          <Stat label="uptime" value={`${tick}s`} color={C.cyan} />
          <Stat label="count" value={count} color={C.violet} />
          <Stat label={`fib(${count})`} value={String(fib).slice(0, 9)} color={C.amber} />
        </div>

        <div style={{ display: "flex", gap: 8 }}>
          <button onClick={() => setCount((c) => c + 1)} style={btn(C.violet)}>+ increment</button>
          <button onClick={() => setCount(0)} style={btn(C.dim)}>reset</button>
          <button onClick={() => setItems((x) => [...x, `effect ${x.length}`])} style={btn(C.cyan)}>+ row</button>
        </div>

        <ul style={{ listStyle: "none", padding: 0, marginTop: 16 }}>
          {items.map((it, i) => (
            <li
              key={i}
              style={{
                display: "flex",
                justifyContent: "space-between",
                padding: "9px 12px",
                background: C.panel,
                border: `1px solid ${C.border}`,
                borderRadius: 8,
                marginBottom: 6,
                fontSize: 13,
              }}
            >
              <span>{it}</span>
              <span style={{ color: C.dim }}>#{i}</span>
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}

function btn(color) {
  return {
    border: `1px solid ${color}`,
    background: "transparent",
    color,
    fontFamily: "inherit",
    fontSize: 12,
    padding: "7px 14px",
    borderRadius: 7,
    cursor: "pointer",
  };
}
src/main.jsx
jsx
import React from "react";
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";

// Render the first frame synchronously, so the page is complete by the time
// its load event fires (the live sample checks the preview's text on load).
const root = createRoot(document.getElementById("root"));
flushSync(() =>
  root.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
  ),
);
index.html
html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>nano · react-studio</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>
server.js
javascript
// A dependency-free static file server for the pre-built Vite dist/. Runs inside
// NanoVM; serves the React app to the preview iframe via the service-worker bridge.
const http = require("http");
const fs = require("fs");
const path = require("path");

// dist/ relative to the working directory (`cd <demo> && node server.js`).
const ROOT = path.resolve("dist");
const TYPES = {
  ".html": "text/html; charset=utf-8",
  ".js": "text/javascript; charset=utf-8",
  ".css": "text/css; charset=utf-8",
  ".json": "application/json",
  ".svg": "image/svg+xml",
  ".map": "application/json",
};

const server = http.createServer(function (req, res) {
  let url = req.url.split("?")[0];
  if (url === "/" || url === "") url = "/index.html";
  const file = path.join(ROOT, url);
  fs.readFile(file, function (err, data) {
    if (err) {
      // SPA fallback → index.html
      fs.readFile(path.join(ROOT, "index.html"), function (e2, html) {
        if (e2) {
          res.writeHead(404);
          res.end("not found");
          return;
        }
        res.writeHead(200, { "Content-Type": TYPES[".html"] });
        res.end(html);
      });
      return;
    }
    res.writeHead(200, { "Content-Type": TYPES[path.extname(file)] || "application/octet-stream" });
    res.end(data);
  });
});

server.listen(3000, function () {
  console.log("react-studio listening on 3000");
});
vite.config.js
javascript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// base: "./" → relative asset URLs, so the build works when served under the
// preview path (/sw/<port>/) instead of the origin root.
export default defineConfig({
  base: "./",
  plugins: [react()],
  build: { assetsInlineLimit: 100000000 }, // inline everything → single JS, no binary assets
});
package.json
json
{
  "name": "nano-react-studio",
  "private": true,
  "type": "module",
  "scripts": { "build": "vite build" },
  "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" },
  "devDependencies": { "@vitejs/plugin-react": "^4.3.4", "vite": "^6.0.7" }
}

Further reading

Other examples