Classic React that just works in a browser — React's prebuilt UMD build, loaded as a
plain <script> global. No bundler, no import map, no transform of React itself.
cargo run -p react-umd
# open http://127.0.0.1:8081/This is the buildless counterpart to react-esm, which has to bundle React
19's CommonJS into ESM with rolldown. React 18 still ships a UMD build that runs in a browser
as-is, so here there's nothing to bundle.
- Single-asset extraction. React's npm package is large (CommonJS sources, several
builds), but a browser needs exactly one file.
main.rsvendors withExtract::File { from: "umd/react.production.min.js", to: "react.js" }— pulling just that one asset out of the package (and the same for react-dom) instead of the whole tree. After vendoring,web/web_modules/holds onlyreact/react.js(~11 KB) andreact-dom/react-dom.js(~130 KB) — nothing else. - Classic UMD, loaded as a global.
web/index.htmlloads those two files with classic<script>tags (react before react-dom), exposingwindow.React/window.ReactDOM.web/app.tsuses those globals viaReact.createElement(no JSX) and is transformed to JS on the fly by web-modules (oxc) — so the source is type-checked TS, but nothing is bundled.
main.rsvendors the two single files intoweb/web_modules/withExtract::File, then serves theweb/tree withweb_modules::dev::serve(compilingapp.tsper request).web/app.tstypes the UMD globals withdeclare const React: typeof import("react")(a type-only query, erased at compile time — nothing is imported at runtime) and renders auseStatecounter withReact.createElement.
cd examples/react-umd
npm ci
npm run test:types # tsc --noEmit (types the UMD globals via @types/react)
npm run test:e2e # Playwright: the UMD globals load, the counter increments, no errors