Write React. Ship direct DOM.
Vidact compiles React-style function components and hooks into direct DOM operations. Components run once when they mount.
npx vidact my-appIn beta. Check React compatibility
State goes straight to the DOM
The compiler works out which expressions depend on each value and writes an updater for them. A state change runs that updater instead of running the component again.
The browser runs Vidact's small runtime. React, the Virtual DOM, the reconciler, and runtime dependency tracking stay out of the bundle.
React runtime
- State changes
- Run component
- Create element tree
- Reconcile
- Update DOM
Vidact
- State changes
- Run selected updater
- Update DOM
What the compiler writes
Compare the component with a readable expansion of its DOM operations, or switch to the exact compiler output.
import { useState } from 'react'export function Counter() { const [count, setCount] = useState(0) return ( <div> <button onClick={() => setCount(count + 1)}> Increment </button> <output>Count: {count}</output> </div> )}The component you write.
export function Counter() { let count = 0 const root = document.createElement('div') const button = document.createElement('button') const output = document.createElement('output') const countText = document.createTextNode('0') button.textContent = 'Increment' output.append('Count: ', countText) root.append(button, output) const updateCount = () => { countText.data = String(count) } button.addEventListener('click', () => { count += 1 // Vidact batches this updater; direct call shown for clarity. updateCount() }) return root}Runtime scheduling and cleanup are omitted for readability.
- Component runs
- 1
- DOM mutations
- 0
- Production bundle
- 8.1 kB
Gzipped, including the runtime.
Increment updates the existing text node.
More compiled behavior
Forms, keyed lists, and conditional branches use the same update model.
import { useState } from 'react'export function Greeting() { const [name, setName] = useState('') return ( <form> <input placeholder="Your name" value={name} onChange={(event) => setName(event.target.value)} /> <p>Hi, {name === '' ? 'stranger.' : `${name}!`}</p> </form> )}Typing updates the existing greeting.
Why now
I came back to a six-year-old experiment
I started Vidact in 2020, then put it aside. Building grep.codemod.com gave me a reason to return to Vidact. That app now runs on Vidact in production.
What Vidact reuses
The compiler is written in Rust and uses React Compiler's analysis infrastructure for AST, scope, HIR, CFG, SSA, and dependency information. Vidact has its own IR, DOM code generator, and runtime.
Build full-stack apps
Vidact Start applies the same compiler model to SSR and hydration, then adds file routes, loaders, and client navigation. This documentation site is compiled with Vidact and runs on Vidact Start.
Read the Start guideimport { defineFileRoute } from '@vidact/start'const loader = async ({ params }) => ({ product: await findProduct(params.productId),})export function ProductRoute({ loaderData }) { return <h1>{loaderData.product.name}</h1>}export const Route = defineFileRoute({ loader, component: ProductRoute,})Unsupported React stays a compile error
Unsupported code fails at build time. Vidact never responds by shipping React or switching to a slower renderer, which makes it a deliberate subset of React today.
Found a React pattern that should compile? Open an issue.