Write React. Ship direct DOM code.
Overview · Quick start · How it works · Packages · Examples · Development
Vidact is a compiler for React-shaped components. You write function components, JSX, and hooks exactly the way you would in a React application. Instead of shipping React, Vidact compiles each component into plain DOM operations: create these elements once, and when this piece of state changes, update this text node.
There is no Virtual DOM, no reconciler, and no React runtime in your bundle.
Warning
Vidact is in beta. APIs are stabilizing and the supported subset of React is growing. Pin @vidact/runtime, @vidact/vite, and @vidact/start to matching versions and run your test suite against the versions you ship.
Components run once, when they mount. The compiler reads each function ahead of time, works out which parts of the output depend on which values, and emits a fixed list of small updaters. A state write runs only the updaters that read that state. The component function is never called again.
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<button onClick={() => setCount((value) => value + 1)}>Increment</button>
<output>Count: {count}</output>
</div>
)
}Compiled by Vidact, this builds a <div>, a <button>, and an <output> once, then keeps one updater that rewrites the text node whenever count changes.
Why Vidact?
- Small bundles. A compiled counter is about 8 kB gzipped with the runtime included, because there is no reconciler to download.
- Predictable updates. A state write runs a known list of updaters, so there are no surprise re-renders, no stale closures, and no
memoto keep things fast. - Familiar API.
useState,useEffect,useContext,useRef, and the rest work the way you expect, and most React components compile without changes. - Loud failures. Code Vidact cannot compile fails the build at the exact source location instead of falling back to a slower path.
- Full stack when you want it. Vidact Start adds file-based routing, loaders, server rendering, hydration, and client navigation.
What Vidact is not. Vidact is not a React renderer and does not run React. There is no element tree, no Fiber, and no React DevTools. Class components, React.Children, and libraries that reach into React internals are not supported. See the React compatibility matrix for the full contract.
The fastest way to start is the project generator:
npx vidact my-appIt asks for a template and leaves you with a project you can run:
| Template | What you get |
|---|---|
spa |
Vite, the Vidact compiler plugin, and a client-rendered entry point |
start |
Vidact Start with file routes, loaders, server rendering, and hydration |
nitro |
The same full-stack app served by Nitro, with a preset for every major host |
Install the runtime, the Vite plugin, and the React-shaped types. react and react-dom are missing from this list on purpose: your source imports from react, and the Vite plugin resolves those imports to the compiled runtime.
pnpm add @vidact/runtime
pnpm add -D @vidact/vite @vidact/react-types @types/react typescript viteAdd the plugin to Vite. It compiles every .tsx file in the project.
// vite.config.ts
import { vidact } from '@vidact/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [vidact()],
})Tell TypeScript to leave JSX alone and to type it with Vidact's React-shaped declarations:
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "@vidact/react-types",
"types": ["@vidact/react-types", "vite/client"]
}
}Mount a component. mountCompiled takes the component itself rather than a JSX element, because a compiled component is a factory that builds its own DOM.
// src/main.ts
import { mountCompiled } from '@vidact/runtime'
import { Counter } from './Counter.tsx'
mountCompiled(Counter, document.querySelector('#app')!)Then run pnpm vite for development or pnpm vite build for a static production build.
Note
The compiler and runtime share a protocol version. Always upgrade @vidact/runtime, @vidact/vite, and @vidact/start together. Mixed versions fail loudly at startup rather than silently misbehaving.
Suspense, transitions, form actions, and retained UI are part of React's API and all work as expected, but they stay out of your bundle until you enable them:
vidact({ features: ['async', 'concurrent'] })| Feature | What it enables |
|---|---|
async |
Suspense, lazy, and use(promise) |
concurrent |
useTransition, startTransition, useDeferredValue, flushSync |
actions |
Function-valued form action, useActionState, useOptimistic, useFormStatus |
retained-ui |
Activity for hiding UI while keeping its state |
Using a feature that is not enabled produces a compile error naming the flag.
Swap the plugin for vidactStart() and add a src/routes directory. Each route module exports a Route with an optional server loader; its result reaches the component as typed loaderData.
// src/routes/index.tsx
import { defineFileRoute, type RouteComponentProps } from '@vidact/start'
const loader = () => ({ greeting: 'Hello from the server' })
export function HomeRoute({ loaderData }: RouteComponentProps<ReturnType<typeof loader>>) {
return <h1>{loaderData.greeting}</h1>
}
export const Route = defineFileRoute({ loader, component: HomeRoute })The server renders HTML with the loader's data, embeds a snapshot, and the client hydrates the existing DOM without rebuilding it. See the @vidact/start README and the Start guides for entries, navigation, data loading, and deployment.
React source
-> React Compiler analysis in Rust (AST, scope, HIR/CFG/SSA, dependencies)
-> Vidact analysis adapter
-> Vidact static updater IR
-> Vanilla DOM codegen
-> @vidact/runtime
@vidact/vitesends untouched TSX to@vidact/compiler, a prebuilt native Node-API addon. Consumers never need Rust or Cargo.- The Rust compiler runs a vendored React Compiler analysis, lowers a static updater graph, and rewrites state, scalar, branch, and keyed-list expressions.
- OXC prints the transformed module and lowers JSX through
@vidact/runtime/jsx-runtime. - At runtime the component constructs its DOM once. A state write marks a compiler-assigned source dirty; updaters are emitted in execution order with static read/write masks, so the browser never discovers dependencies or diffs a tree.
React Compiler is an analysis dependency, not Vidact's renderer or code generator. Its internal types terminate at a narrow adapter, and the rest of Vidact uses its own stable facts and IR. The architecture notes record these decisions and the analysis boundary explains the integration constraints.
Reachable node_modules packages that declare react as a dependency are compiled automatically, which is how published component libraries such as Base UI and shadcn components work without an allowlist.
| Package | Role |
|---|---|
vidact |
Project generator with spa, start, and nitro templates |
@vidact/runtime |
Fine-grained direct-DOM runtime: scheduler, state slots, roots, keyed ranges |
@vidact/vite |
Vite plugin that compiles .tsx files and resolves react imports |
@vidact/react-types |
JSX and hook types describing what Vidact actually does, built on @types/react |
@vidact/start |
File routes, loaders, SSR, hydration, and client navigation |
@vidact/compiler |
Native compiler bindings and the vidactc CLI, for tooling authors |
@vidact/test-support |
act and DOM mutation assertions for Vitest browser tests |
The Rust side lives in crates/vidact-compiler (analysis facts and updater IR) and crates/vidact-node (the Node-API adapter).
Every example is ordinary React-shaped TSX. Run them from the repository root after the development setup.
| Example | Command | Highlights |
|---|---|---|
| TodoMVC | pnpm dev:todomvc |
Array state, keyed lists, and events with no Virtual DOM |
| Shop | pnpm dev:shop |
Streaming SSR, "use client" boundaries, Suspense, Tailwind, shadcn and Base UI from node_modules |
| Start | pnpm dev:start |
Nested layouts, typed loaders, dynamic params, route endpoints, hydration |
| Docs | pnpm dev:docs |
The documentation site itself, built with Vidact Start and headless Fumadocs, deployed on Nitro |
The user documentation is written in examples/docs/content/docs: a quick start, a Learn section covering one concept per page, Vidact Start guides, a migration guide from React, a testing guide, and per-package references.
Requirements: Rust 1.96, Node.js 24+, pnpm 10, and Playwright's Chromium, Firefox, and WebKit installs.
scripts/prepare-oxc.sh # initialize the pinned Oxc submodule and apply the React Compiler patch
pnpm install
pnpm build:packages
pnpm typecheck
cargo test --workspace
pnpm test:browserpnpm check runs everything CI runs: lint and format gates, type checks, the Rust suite, the cross-browser corpus, package and example verification, production size budgets, and compiler and runtime benchmarks.
Tip
Ordinary builds do not need Go. Only maintainers editing the checked-in Oxc patch install git-go-patch with go install github.com/microsoft/go-infra/cmd/git-go-patch@v0.0.16. See patched Oxc submodule.
Every pull request needs a changeset. Run pnpm changeset for a published package change, or pnpm changeset --empty for repository-only work. Merged changesets feed the automated Version Packages pull request described in the release policy.
Repository layout:
crates/: the Rust compiler and its Node-API adapterpackages/: the published npm packagesexamples/: runnable applications, including the docs sitetests/browser: compiled Vitest Browser corpora run in Chromium, Firefox, and WebKitdocs/architecture: durable architecture decisions and upstream constraintsdocs/lint-rules: the ast-grep rules enforced across the workspacevendor/oxcandpatches/oxc: the pinned Oxc submodule and Vidact's patch series