DAVAUX
Alpha

Reactive Components

Davaux pages produce zero client-side JavaScript by default. Every route is rendered to HTML on the server and sent as a plain document. When you need interactivity — a counter, a dropdown, a live search box — you reach for reactive components.

A reactive component is a JSX component that:

  • Built on top of the Island architecture
  • Auto-detected by pragma — no src/islands/ directory required
  • Runs on the server to produce its initial HTML (no blank flash)
  • Is automatically bundled for the browser and hydrated in place
  • Works as an isolated interactive island — the rest of the page stays as static HTML
  • Can import and use other JSX components to build component trees

Creating a reactive component

Any component file with the /** @jsxImportSource davaux/client */ pragma is automatically detected and included in the client bundle. It can live anywhere under src/ — typically beside the routes that use it.

Add the pragma so the file uses client-side JSX, and wrap the export with reactive() so TypeScript accepts it in server JSX files:

// src/components/Counter.tsx
/** @jsxImportSource davaux/client */
import { reactive } from 'davaux'
import { createSignal } from 'davaux/client'

function Counter({ initial = 0 }: { initial?: number }) {
  const [count, setCount] = createSignal(initial)

  return (
    <div class="counter">
      <button onClick={() => setCount(count() - 1)}>−</button>
      <span>{() => count()}</span>
      <button onClick={() => setCount(count() + 1)}>+</button>
    </div>
  )
}

export default reactive(Counter)

reactive() is a zero-cost type bridge — it has no runtime effect. Its only job is to tell TypeScript that the component is server-JSX-compatible while signals and DOM event handlers work correctly in the browser.

Composing components

Reactive components can import and use other client-side JSX components freely. Sub-components only need the /** @jsxImportSource davaux/client */ pragma — reactive() is only required on the components you import directly into server JSX files:

// src/components/IconButton.tsx
/** @jsxImportSource davaux/client */

interface IconButtonProps {
  onClick: () => void
  children: unknown
}

export function IconButton({ onClick, children }: IconButtonProps) {
  return <button class="icon-btn" onClick={onClick}>{children}</button>
}
// src/components/Counter.tsx
/** @jsxImportSource davaux/client */
import { reactive } from 'davaux'
import { createSignal } from 'davaux/client'
import { IconButton } from './IconButton.js'

function Counter({ initial = 0 }: { initial?: number }) {
  const [count, setCount] = createSignal(initial)

  return (
    <div class="counter">
      <IconButton onClick={() => setCount(count() - 1)}>−</IconButton>
      <span>{() => count()}</span>
      <IconButton onClick={() => setCount(count() + 1)}>+</IconButton>
    </div>
  )
}

export default reactive(Counter)

IconButton is a plain client JSX function — no reactive() needed because it is never used directly in a server JSX file. Only Counter, which crosses the server/client boundary at the page level, needs reactive().

Note: You can import a server JSX component (one without reactive()) into a reactive component, it works at runtime — esbuild recompiles it for the client. The client JSX.Element type includes Promise<string> alongside Node so TypeScript won't emit any errors. Components using server-only APIs (request context, file system, etc.) are the exception — those cannot run in the browser regardless.

Using a reactive component in a page

Import and use it exactly like any other JSX component:

// src/routes/index.page.tsx
import { definePage } from 'davaux'
import Counter from '../components/Counter.js'

export default definePage((ctx) => {
  return (
    <main>
      <h1>Hello from the server</h1>
      <Counter initial={5} />
    </main>
  )
})

Davaux renders Counter to its initial HTML on the server, then ships it to the browser for hydration. There is no loading flash — the component is interactive as soon as the small JS bundle evaluates.

Props passed to a reactive component must be JSON-serialisable (strings, numbers, booleans, plain objects and arrays) so they can be embedded in the server-rendered HTML and re-read by the client. See Islands for the full rules.

Sharing state between components

Because all reactive components on a page share a single client bundle, you can share state between them with a module-level signal:

// src/lib/store.ts
import { createSignal } from 'davaux/client'

export const [cartCount, setCartCount] = createSignal(0)
// src/components/CartButton.tsx
/** @jsxImportSource davaux/client */
import { reactive } from 'davaux'
import { cartCount, setCartCount } from '../lib/store.js'

function CartButton() {
  return (
    <button onClick={() => setCartCount(cartCount() + 1)}>
      Cart ({() => cartCount()})
    </button>
  )
}
export default reactive(CartButton)
// src/components/CartBadge.tsx
/** @jsxImportSource davaux/client */
import { reactive } from 'davaux'
import { cartCount } from '../lib/store.js'

function CartBadge() {
  return <span class="badge">{() => cartCount()}</span>
}
export default reactive(CartBadge)

Both components react to the same signal — updates in one are instantly reflected in the other. For nested state, use createStore instead. See Signals & Store for the full API.

Next steps

That covers day-to-day reactive component authoring. For the underlying hydration model — how data-island markers work, the explicit island() API, the src/islands/ directory, and shipping components from a plugin — see Islands.