DocumentationFramework guide

React islands

TSX pages, Markdown layouts, and ordinary components produce static HTML. Put only a browser-interactive subtree under src/islands, then use it like a normal typed React component.

// src/islands/counter.tsx
import { useState } from 'react'
import { defineIsland } from '@briansunter/nib'

function Counter({ initialCount }: { initialCount: number }) {
  const [count, setCount] = useState(initialCount)
  return <button onClick={() => setCount((value) => value + 1)}>Count: {count}</button>
}

export default defineIsland('counter', Counter)

The ID must match the module path below src/islands. For example, src/islands/cart/summary.tsx uses cart/summary.

Import the definition from a TSX page or layout:

// src/pages/example/page.tsx
import Counter from '../../islands/counter'

export default function Page() {
  return <Counter initialCount={0} hydrate="load" />
}

Hydration timing

Value Behavior
load Hydrates immediately and is the default.
idle Waits for idle time, with a timer fallback.
visible Waits until an island child approaches the viewport; text-only roots use their parent as the observation target.

Every island is rendered into the generated HTML first. The browser then loads that island module and hydrates its independent React root. A route without islands has no React client entry.

Props and boundaries

Island props must be JSON-serializable. Strings, booleans, finite numbers, null, arrays, plain objects, and absent optional properties are supported. Functions, React nodes, class instances, dates, maps, sets, cycles, explicit undefined, and non-finite numbers fail type checking or the build.

A top-level island owns its own state and context tree. An island may render another island definition; Nib composes the child into the same React root, and the outermost island's hydrate strategy controls the whole subtree. This lets interactive pieces share state and context through ordinary React composition without creating conflicting nested hydration roots.

The component must produce the same initial markup on the server and in the browser. Read from window, storage, media queries, or other browser-only APIs in an event handler or useEffect, not while rendering. This preserves the static fallback and avoids hydration mismatches.

Read the repository's docs/architecture.md for the rendering pipeline and design constraints. The separate HTML pages proposal explores typed island bindings for a future page.html route format; it is not part of the current API.