Islands

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

An island is a JSX component that is:

  1. Rendered on the server to its initial HTML (no blank flash)
  2. Shipped to the browser as a small JS bundle
  3. Hydrated independently — the rest of the page stays as static HTML

Creating an island

Islands live in src/islands/. Create a .tsx file and wrap your component with the island() function from davaux:

// src/islands/Counter.tsx
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)

The island() wrapper registers the component for hydration and handles the client/server split automatically.

Using an island in a page

Import and use the island exactly like any other JSX component:

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

export default definePage((ctx) => {
  return (
    <main>
      <h1>Hello from the server</h1>
      <p>The counter below is interactive:</p>
      <Counter initial={5} />
    </main>
  )
})

Davaux renders Counter on the server first (producing its initial HTML), then serializes the props and ships the component to the browser for hydration.

How hydration works

When Davaux renders an island server-side it emits a wrapper element containing:

  • The pre-rendered HTML (visible immediately, no layout shift)
  • A data-props attribute with the JSON-serialized props
  • A data-island attribute naming the component

The client bundle picks this up and calls hydrate() on each island marker, restoring full reactivity.

Because the initial HTML is already present, there is no loading flash. The island becomes interactive as soon as the tiny JS bundle evaluates — typically within a few dozen milliseconds.

Props must be JSON-serializable

Since props are serialized to a data-props JSON string, they must round-trip through JSON.stringify / JSON.parse. Supported types:

  • Strings, numbers, booleans, null
  • Plain objects (including nested)
  • Arrays

Not supported:

  • Date objects (serialize to ISO string yourself: date.toISOString())
  • Functions
  • Class instances
  • undefined values (omit the key instead)
// OK
<MyIsland
  name="Alice"
  count={42}
  tags={['a', 'b']}
  config={{ dark: true }}
/>

// Not OK — will throw at build time or produce wrong results
<MyIsland
  createdAt={new Date()}        // serialize to string first
  onClick={() => console.log()} // functions cannot be serialized
/>

Shared state between islands

Because all islands on a page are bundled into a single client script, you can share state between them using module-level singletons. Create a shared signal file:

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

export const [cartCount, setCartCount] = createSignal(0)

Then import it in any island:

// src/islands/CartButton.tsx
import { island } from 'davaux'
import { cartCount, setCartCount } from './store.ts'

function CartButton() {
  return (
    <button onClick={() => setCartCount(cartCount() + 1)}>
      Cart ({() => cartCount()})
    </button>
  )
}
export default island(CartButton)
// src/islands/CartBadge.tsx
import { island } from 'davaux'
import { cartCount } from './store.ts'

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

Both islands react to the same signal — updates in one are instantly reflected in the other.

Using createStore for shared reactive state

For more complex shared state with nested objects, use createStore:

// src/islands/appStore.ts
import { createStore } from 'davaux/client'

interface AppState {
  user: { name: string; loggedIn: boolean } | null
  theme: 'light' | 'dark'
  notifications: string[]
}

export const [store, setStore] = createStore<AppState>({
  user: null,
  theme: 'light',
  notifications: [],
})

Read and update with path-based setters:

// Login button island
setStore('user', { name: 'Alice', loggedIn: true })

// Theme toggle island
setStore('theme', (prev) => prev === 'light' ? 'dark' : 'light')

// Add notification
setStore('notifications', (prev) => [...prev, 'New message!'])