Islands
This page covers the island hydration runtime and its advanced controls. If you are looking for how to write and use reactive components day-to-day, start with Reactive Components.
How hydration works
When Davaux renders a reactive component server-side it emits a wrapper element containing:
- The pre-rendered HTML (visible immediately, no layout shift)
- A
data-propsattribute with the JSON-serialised props - A
data-islandattribute naming the component
<div data-island="Counter" data-props='{"initial":5}' style="display:contents">
<div class="counter">
<button>−</button><span>5</span><button>+</button>
</div>
</div>
The wrapper uses display: contents — it is completely layout-transparent and never breaks flexbox, grid, or any other layout context.
The client bundle picks up each [data-island] element and calls hydrate() on it, restoring full signal reactivity. Because the initial HTML is already present, there is no loading flash. The component becomes interactive as soon as the tiny JS bundle evaluates — typically within a few dozen milliseconds.
Disposing an island
Every hydrated island runs inside its own reactive scope — the same createRoot mechanism davaux/client exposes directly (see Signals & Store). If you remove an island's element from the DOM yourself — closing a modal, building your own client-side router — call disposeIsland() first so its effects stop running and any onCleanup() handlers (timers, WebSocket connections, createResource polling) actually fire:
import { disposeIsland } from 'davaux/client'
const el = document.querySelector('[data-island="Modal"]')
if (el) {
disposeIsland(el)
el.remove()
}
disposeIsland() is a no-op if the element was never hydrated, or has already been disposed — safe to call defensively.
Props must be JSON-serialisable
Since props are embedded as a JSON string in data-props, they must round-trip through JSON.stringify / JSON.parse. Supported types:
- Strings, numbers, booleans,
null - Plain objects (including nested)
- Arrays
Not supported:
Dateobjects — serialize to ISO string yourself:date.toISOString()- Functions
- Class instances
undefinedvalues (omit the key instead)
// OK
<Counter
name="Alice"
count={42}
tags={['a', 'b']}
config={{ dark: true }}
/>
// Not OK — will throw at build time or produce wrong results
<Counter
createdAt={new Date()} // serialize to string first
onClick={() => console.log()} // functions cannot be serialized
/>
The island() function
The reactive() wrapper handles most cases automatically — the build pipeline detects the component and generates the island marker at build time. When you need explicit control over the island ID or want to wrap a component that isn't auto-detected, use island() directly:
// src/components/Counter.tsx
/** @jsxImportSource davaux/client */
import { island } 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 island(Counter, 'my-counter')
The second argument is an optional stable ID. If omitted, island() uses the function name. Stable IDs are useful when minification would otherwise rename the function.
island() and reactive() produce the same runtime output — the data-island wrapper. The difference is that island() is explicit and runs at call time, while reactive() delegates to the build pipeline.
Explicit src/islands/ directory
If you prefer a dedicated directory for island components, src/islands/ is unconditionally scanned — every file in it is included in the client bundle regardless of what it imports. The pragma and reactive() are still needed for correct TypeScript types, but the from 'davaux/client' import is not required for detection.
src/
islands/
Counter.tsx ← always bundled
Dropdown.tsx ← always bundled
components/
Header.tsx ← server-only, not bundled
CartBadge.tsx ← bundled because it imports from 'davaux/client'
The auto-detection approach (any file under src/ that imports davaux/client) is the recommended default since it lets components live beside the routes that use them. src/islands/ is useful when you want an explicit inventory of everything that ships to the client.
Islands from plugins
Packages can ship island components in two ways.
islandsDir — declare an absolute path to a directory. All files in it are included unconditionally, the same as the local src/islands/:
import { resolve } from 'node:path'
import type { DavauxPlugin } from 'davaux/config'
export function myPlugin(): DavauxPlugin {
return {
name: 'my-plugin',
islandsDir: resolve(import.meta.dirname, 'islands'),
}
}
srcDirs — declare source directories to scan for reactive components (auto-detected via from 'davaux/client'). Use this when reactive and server-only components share the same dist directory:
export function myPlugin(): DavauxPlugin {
return {
name: 'my-plugin',
srcDirs: [resolve(import.meta.dirname, 'components')],
}
}
From the app's perspective both approaches are identical — plugin islands work exactly like local reactive components:
import SomeWidget from '@my-org/my-plugin/islands/SomeWidget.tsx'
export default definePage(() => (
<main>
<SomeWidget />
</main>
))
See Plugins for the full plugin authoring guide.