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 clientJSX.Elementtype includesPromise<string>alongsideNodeso 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.