Vidact

Write React. Ship direct DOM.

Vidact compiles React-style function components and hooks into direct DOM operations. Components run once when they mount.

Get started npx vidact my-app

In 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

  1. State changes
  2. Run component
  3. Create element tree
  4. Reconcile
  5. Update DOM

Vidact

  1. State changes
  2. Run selected updater
  3. Update DOM

What the compiler writes

Compare the component with a readable expansion of its DOM operations, or switch to the exact compiler output.

Source
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.

Result preview
Count: 0
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.

Greeting.tsx
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>  )}
Result

Hi, stranger.

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 guide
src/routes/products/$productId.tsx
import { 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.

See supported APIs