cd /news/developer-tools/how-i-compile-react-shaped-tsx-witho… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-73303] src=dev.to β†— pub= topic=developer-tools verified=true sentiment=Β· neutral

How I compile React-shaped TSX without React or hydration

A developer created Kudzu, an experimental HTML-first TSX framework that compiles React-shaped TSX into static HTML and capability-specific JavaScript, eliminating the need for React, a virtual DOM, or hydration on the client. The framework executes function components during the build and ships only the JavaScript required for each route's actual behavior.

read7 min views1 publishedJul 25, 2026

I started building Kudzu while making static websites with AI.

AI coding tools have become very good at producing React-shaped TSX, and I have become used to reviewing code in that form. Function components, props, JSX, and event handlers are often easier for me to understand and verify than scattered DOM queries and imperative JavaScript mutations.

But I was still building static pages.

I wanted to keep TSX as the authoring and code-review format without automatically shipping React, a virtual DOM, hydration, or a browser-side component tree.

Kudzu grew from that idea:

Write familiar TSX, execute components during the build, and ship ordinary HTML with only the JavaScript each route actually needs.

Kudzu is an experimental, HTML-first TSX framework.

Website: kudzujs.cloud

GitHub: github.com/kudzujs/kudzu

Consider a blog, documentation site, newsletter, or product landing page.

Most of the page is already known during the build:

TSX is a convenient way to author and review that structure.

function PostCard({ title, description, href }: {
  title: string
  description: string
  href: string
}) {
  return (
    <article>
      <h2><a href={href}>{title}</a></h2>
      <p>{description}</p>
    </article>
  )
}

The component model is useful for authoring, but that does not necessarily mean the browser needs a component runtime.

For a static page, I wanted the output to remain ordinary HTML.

<article>
  <h2><a href="/posts/hello">Hello</a></h2>
  <p>My first article.</p>
</article>

I also wanted interactive pages to receive only the JavaScript required for their actual behavior.

Kudzu treats components as build-time authoring units.

React-shaped TSX
        ↓
Kudzu compiler
        ↓
Static HTML + CSS + capability-specific ESM

Function components execute during the build.

The browser does not receive:

A completely static route receives no client JavaScript.

A Kudzu page lives in src/pages

.

// src/pages/index.tsx

export default function HomePage() {
  return (
    <main>
      <h1>Hello from Kudzu</h1>
      <p>This page becomes static HTML.</p>
    </main>
  )
}

Build the project:

npm run build

The output is written to dist

.

dist/
β”œβ”€ index.html
└─ assets/
   └─ style.css

Because the page has no browser behavior, Kudzu does not add a JavaScript runtime.

Kudzu supports a React-shaped useState

API.

import { useState } from "@kudzujs/core"

export default function Counter() {
  const [count, setCount] = useState(0)

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  )
}

The compiler identifies:

The component is executed during the build to produce the initial HTML.

The browser receives a small behavior plan that updates the relevant DOM node directly.

click
  β†’ update logical state
  β†’ queue a DOM commit
  β†’ patch the bound text node

The browser does not rerun the component.

Kudzu deliberately differs from React's state snapshot behavior.

function increaseTwice() {
  setCount(count + 1)
  setCount(count + 1)
}

In Kudzu:

The handler above increments the value by two and patches the DOM once.

This model gives event handlers normal sequential JavaScript behavior without requiring a browser component rerender.

Kudzu has two event compilation paths.

Simple setter-only handlers compile to compact behavior commands.

<button onClick={() => setCount(count + 1)}>
  Increase
</button>

Normal synchronous or asynchronous handlers compile to external ESM.

async function loadStatus() {
  setStatus("")

  try {
    const response = await fetch("/api/status")
    const result = await response.json()
    setStatus(result.status)
  } catch {
    setStatus("failed")
  }
}

Kudzu does not use:

eval

;new Function

;Relative TypeScript helpers can also be bundled into the generated handler module.

import { normalizeStatus } from "../lib/status"

async function loadStatus() {
  const response = await fetch("/api/status")
  const result = await response.json()

  setStatus(normalizeStatus(result))
}

State can be used in text, attributes, styles, and form properties.

const [open, setOpen] = useState(false)

return (
  <button
    className={open ? "active" : "idle"}
    aria-expanded={open}
    onClick={() => setOpen(!open)}
  >
    {open ? "Close" : "Open"}
  </button>
)

Kudzu compiles the expressions into direct DOM updates.

open changes
  β†’ patch class
  β†’ patch aria-expanded
  β†’ patch text

The entire component is not rerendered.

Inline conditional children compile to bounded DOM ranges.

const [open, setOpen] = useState(false)

return (
  <>
    <button onClick={() => setOpen(!open)}>
      Toggle menu
    </button>

    {open && (
      <nav>
        <a href="/docs">Docs</a>
        <a href="/example">Examples</a>
      </nav>
    )}
  </>
)

Both branches are prepared during the build. The browser mounts or removes only the relevant DOM range.

There is still no virtual DOM or component tree.

Kudzu also supports a constrained form of keyed list rendering.

const [items, setItems] = useState([
  { id: 1, name: "Oak" },
  { id: 2, name: "Pine" }
])

return (
  <ul>
    {items.map(item => (
      <li key={item.id}>
        {item.name}
        <button
          onClick={() =>
            setItems(items.filter(entry => entry.id !== item.id))
          }
        >
          Remove
        </button>
      </li>
    ))}
  </ul>
)

The initial list is emitted as static HTML.

When state changes, Kudzu moves, inserts, updates, or removes the keyed DOM nodes directly.

Existing keyed nodes are preserved instead of remounted.

Async components can load data during the build.

export default async function ProductsPage() {
  const products = await fetch(
    "https://example.com/api/products"
  ).then(response => response.json())

  return (
    <main>
      <h1>Products</h1>

      <ul>
        {products.map(product => (
          <li>{product.name}</li>
        ))}
      </ul>
    </main>
  )
}

When the route and data are available during the build, the result is complete static HTML.

No browser fetch or hydration is required.

Build-known dynamic routes use getStaticPaths()

.

// src/pages/posts/[slug].tsx

export function getStaticPaths() {
  return [
    {
      params: { slug: "hello-kudzu" },
      props: { title: "Hello Kudzu" }
    },
    {
      params: { slug: "static-html" },
      props: { title: "Static HTML" }
    }
  ]
}

export default function Post({
  title
}: {
  title: string
}) {
  return <h1>{title}</h1>
}

The build produces:

dist/posts/hello-kudzu/index.html
dist/posts/static-html/index.html

The result can be deployed to an ordinary static host.

Sometimes data or browser APIs are only available after the document loads.

Kudzu supports a deliberately constrained useEffect

form.

import { useEffect, useState } from "@kudzujs/core"

export default function StatusPage() {
  const [status, setStatus] = useState("")

  useEffect(() => {
    fetch("/api/status")
      .then(response => response.json())
      .then(result => setStatus(result.status))
  }, [])

  return <p>Status: {status}</p>
}

The effect is not executed during static rendering.

Kudzu emits a route-specific effect entry that updates the existing logical state and bound DOM nodes.

The component itself is not shipped or rerun.

Kudzu does not give every interactive route the same runtime.

A route receives only the capabilities it uses.

Static route          β†’ HTML only
State commands        β†’ compact behavior runtime
Reactive bindings     β†’ binding capability
Conditional DOM       β†’ bounded DOM operations
Keyed lists           β†’ list capability
Normal handlers       β†’ external handler ESM
Effects               β†’ route-specific effect entry
Runtime parameters    β†’ route-specific pathname reader

A static route remains JavaScript-free even if another route in the same project is interactive.

Kudzu writes deployable artifacts to dist

.

A minimal Cloudflare configuration looks like this:

{
  "name": "my-kudzu-site",
  "compatibility_date": "2026-07-20",
  "assets": {
    "directory": "dist"
  }
}

Build and deploy:

npm run build
npx wrangler deploy

The Kudzu website itself is deployed using Cloudflare static assets.

The project also uses build-time generation for:

Vanilla JavaScript is not the problem.

For small behavior, it is often the smallest and best browser output.

The problem I was trying to solve was authoring and review.

When working with AI-generated code, I found this form easier to inspect:

<PostCard
  title={post.title}
  description={post.description}
  href={`/posts/${post.slug}`}
/>

than a collection of unrelated operations:

const article = document.createElement("article")
const heading = document.createElement("h2")
const link = document.createElement("a")

link.href = `/posts/${post.slug}`
link.textContent = post.title
heading.append(link)
article.append(heading)

The browser may ultimately need ordinary DOM operations.

I just did not want to author and review the entire site at that level.

Kudzu uses TSX as the input language and ordinary HTML and DOM operations as the output.

Kudzu is not a drop-in React replacement.

It supports a statically analyzable React-shaped subset.

Unsupported patterns fail during the build instead of silently causing Kudzu to ship a general component runtime.

Kudzu currently does not provide:

The goal is not to run every React application unchanged.

The goal is to preserve familiar TSX authoring where it can be compiled safely into static HTML and direct browser behavior.

Kudzu is experimental, and the supported TSX surface may still change.

The current implementation includes:

I am especially interested in feedback about:

Create a project:

npm create kudzu@latest my-app
cd my-app
npm run dev

Build the static output:

npm run build

The generated files are available in:

dist/

The build execution plan is available in:

.kudzu/kudzu-plan.json

Website: https://kudzujs.cloud/

Examples: https://kudzujs.cloud/example

Documentation: https://kudzujs.cloud/docs

GitHub: https://github.com/kudzujs/kudzu

── more in #developer-tools 4 stories Β· sorted by recency
── more on @kudzu 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/how-i-compile-react-…] indexed:0 read:7min 2026-07-25 Β· β€”