Skip to content

Islands ​

APIs for working with the islands architecture - selective hydration for interactive components.

Note: For most use cases, you can use the 'use client' directive to mark components for hydration without manual registration. The APIs below are for the advanced createIsland() approach, which gives you control over hydration timing (idle, visible, media, etc.). See Islands Architecture for when to use each approach.

Import ​

ts
import {
  // Island registration and hydration
  islandRegistry,
  hydrateIslands,
  registerIslandComponent,
  getIslandComponent,
  registerIslandComponents,
  createIsland,
  initializeIslands,
  cleanupIslands,

  // Hydration utilities
  parseHydrationDirective,
  createHydrationTrigger,
  stripHydrationProps,
  generateIslandId,
  resetIslandCounter,
  getIslandCount,
  shouldHydrate,
} from '@ereo/client'

// Types
import type { IslandRegistration, HydrationProps } from '@ereo/client'

registerIslandComponent ​

Registers a component for island hydration.

Signature ​

ts
function registerIslandComponent(
  name: string,
  component: ComponentType<any>
): void

Example ​

ts
// src/client.ts
import { registerIslandComponent } from '@ereo/client'
import Counter from './islands/Counter'
import SearchBox from './islands/SearchBox'

registerIslandComponent('Counter', Counter)
registerIslandComponent('SearchBox', SearchBox)

registerIslandComponents ​

Registers multiple components at once.

Signature ​

ts
function registerIslandComponents(
  components: Record<string, ComponentType<any>>
): void

Example ​

ts
import { registerIslandComponents } from '@ereo/client'
import * as Islands from './islands'

registerIslandComponents({
  Counter: Islands.Counter,
  SearchBox: Islands.SearchBox,
  ShoppingCart: Islands.ShoppingCart,
  ThemeToggle: Islands.ThemeToggle
})

getIslandComponent ​

Retrieves a registered component by name.

Signature ​

ts
function getIslandComponent(name: string): ComponentType<any> | undefined

Example ​

ts
const Counter = getIslandComponent('Counter')

if (Counter) {
  // Component is registered
}

hydrateIslands ​

Hydrates all islands on the page. Finds elements with data-island attribute and hydrates them based on their strategy.

Signature ​

ts
function hydrateIslands(): Promise<void>

Example ​

ts
import { hydrateIslands } from '@ereo/client'

// Hydrate all islands on the page
await hydrateIslands()

How It Works ​

  1. Finds all elements with [data-island] attribute
  2. Reads component name from data-component
  3. Parses props from data-props (JSON)
  4. Gets hydration strategy from data-strategy
  5. Creates appropriate hydration trigger based on strategy
  6. Hydrates with React when trigger fires

initializeIslands ​

Initializes the islands system. Called automatically by initClient().

Signature ​

ts
function initializeIslands(): void

Example ​

ts
// Usually handled by initClient()
import { initializeIslands } from '@ereo/client'

initializeIslands()

cleanupIslands ​

Cleans up all hydrated islands.

Signature ​

ts
function cleanupIslands(): void

Example ​

ts
// Cleanup before unmounting
cleanupIslands()

createIsland ​

Creates an island wrapper component for SSR. This registers the component and returns a wrapper that adds hydration data attributes.

Signature ​

ts
function createIsland<P extends Record<string, unknown>>(
  component: ComponentType<P>,
  name: string
): ComponentType<P & HydrationProps>

Example ​

tsx
import { createIsland } from '@ereo/client'
import Counter from './Counter'

// Create an island wrapper
const CounterIsland = createIsland(Counter, 'Counter')

// Use in your components
function Page() {
  return (
    <div>
      <h1>Static Content</h1>
      <CounterIsland initialCount={5} client:visible />
    </div>
  )
}

HydrationProps ​

ts
interface HydrationProps {
  'client:load'?: boolean    // Hydrate immediately on page load
  'client:idle'?: boolean    // Hydrate when browser is idle
  'client:visible'?: boolean // Hydrate when element is visible
  'client:media'?: string    // Hydrate when media query matches
  'client:only'?: boolean    // Only render on client (no SSR)
}

islandRegistry ​

The global island registry instance that tracks all islands on the page.

Interface ​

ts
interface IslandRegistration {
  id: string
  component: ComponentType<any>
  props: Record<string, unknown>
  strategy: 'load' | 'idle' | 'visible' | 'media' | 'none'
  media?: string
  element: Element
  hydrated: boolean
}

class IslandRegistry {
  // Register an island for hydration
  register(
    id: string,
    component: ComponentType<any>,
    props: Record<string, unknown>,
    strategy: HydrationStrategy,
    element: Element,
    media?: string
  ): void

  // Get an island by ID
  get(id: string): IslandRegistration | undefined

  // Mark an island as hydrated
  markHydrated(id: string): void

  // Check if an island is hydrated
  isHydrated(id: string): boolean

  // Set cleanup function for an island
  setCleanup(id: string, cleanup: () => void): void

  // Cleanup a specific island
  cleanup(id: string): void

  // Cleanup all islands
  cleanupAll(): void

  // Get all registered islands
  getAll(): IslandRegistration[]

  // Get islands by strategy
  getByStrategy(strategy: HydrationStrategy): IslandRegistration[]

  // Get pending (not hydrated) islands
  getPending(): IslandRegistration[]
}

Example ​

ts
import { islandRegistry } from '@ereo/client'

// Get all registered islands
const allIslands = islandRegistry.getAll()
console.log(`${allIslands.length} islands registered`)

// Get pending islands
const pending = islandRegistry.getPending()
console.log(`${pending.length} islands waiting to hydrate`)

// Check specific island
if (islandRegistry.isHydrated('island-1')) {
  console.log('Island is hydrated')
}

// Get islands by strategy
const visibleIslands = islandRegistry.getByStrategy('visible')

Island Component Pattern ​

Basic Island ​

tsx
// islands/Counter.tsx
import { useState } from 'react'

export default function Counter({ initialCount = 0 }) {
  const [count, setCount] = useState(initialCount)

  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  )
}

Using in Routes ​

There are two approaches:

Approach 1: 'use client' directive (recommended)

tsx
// app/components/Counter.tsx
'use client'
import { useState } from 'react'

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

// app/routes/index.tsx
import Counter from '~/components/Counter'

export default function Home() {
  return (
    <div>
      <h1>Welcome</h1>
      <Counter client:load initialCount={5} />
    </div>
  )
}

Approach 2: createIsland() wrapper

tsx
// app/routes/index.tsx
import { createIsland } from '@ereo/client'
import CounterBase from '~/components/Counter'

const Counter = createIsland(CounterBase, 'Counter')

export default function Home() {
  return (
    <div>
      <h1>Welcome</h1>
      <Counter client:visible initialCount={5} />
    </div>
  )
}

Hydration Directives ​

client:load ​

Hydrate immediately on page load.

tsx
<Counter client:load />

client:idle ​

Hydrate when the browser is idle (requestIdleCallback).

tsx
<Counter client:idle />

client:visible ​

Hydrate when the element enters the viewport (IntersectionObserver with 200px margin).

tsx
<Counter client:visible />

client:media ​

Hydrate when a media query matches.

tsx
<Counter client:media="(max-width: 768px)" />

No directive ​

Omitting directives means the component is server-rendered only (never hydrated).

tsx
<Counter />

Internal data attributes ​

createIsland() and the 'use client' plugin produce these attributes on the wrapper <div> during SSR. You don't write these yourself:

  • data-island — unique island ID (e.g., "island-1")
  • data-component — component name (e.g., "Counter")
  • data-strategy — hydration strategy (e.g., "visible")
  • data-props — serialized JSON props
  • data-media — media query (for client:media)

Advanced Patterns ​

Conditional Registration ​

ts
// Only register on client
if (typeof window !== 'undefined') {
  registerIslandComponent('Counter', Counter)
}

Lazy Loading Islands ​

ts
// Lazy load island components
const LazyCounter = lazy(() => import('./islands/Counter'))

registerIslandComponent('Counter', LazyCounter)

Island with Context ​

tsx
// islands/ThemedButton.tsx
import { useContext } from 'react'
import { ThemeContext } from '../context/theme'

export default function ThemedButton({ children }) {
  const theme = useContext(ThemeContext)

  return (
    <button className={theme === 'dark' ? 'btn-dark' : 'btn-light'}>
      {children}
    </button>
  )
}

Wrap with provider:

tsx
// client.ts
function ThemedButtonWrapper(props) {
  return (
    <ThemeProvider>
      <ThemedButton {...props} />
    </ThemeProvider>
  )
}

registerIslandComponent('ThemedButton', ThemedButtonWrapper)

Shared State Between Islands ​

tsx
// lib/store.ts
import { signal } from '@ereo/state'
export const count = signal(0)

// islands/CounterDisplay.tsx
import { count } from '../lib/store'

export default function CounterDisplay() {
  return <span>{count.get()}</span>
}

// islands/CounterButton.tsx
import { count } from '../lib/store'

export default function CounterButton() {
  return (
    <button onClick={() => count.set(count.get() + 1)}>
      Increment
    </button>
  )
}

Custom Hydration Strategy ​

ts
// Hydrate based on user interaction
function hydrateOnInteraction(element: Element) {
  const events = ['click', 'touchstart', 'focus']

  const handler = () => {
    hydrateIslands(`[data-island="${element.dataset.island}"]`)
    events.forEach(e => element.removeEventListener(e, handler))
  }

  events.forEach(e => element.addEventListener(e, handler, { once: true }))
}

Debugging ​

Enable island debugging:

ts
// In development
if (process.env.NODE_ENV === 'development') {
  window.__EREO_ISLAND_DEBUG__ = true
}

This logs:

[Islands] Registered: Counter
[Islands] Hydrating: Counter (strategy: idle)
[Islands] Hydrated: Counter in 12ms

Hydration Utilities ​

These utilities are used internally but are exported for advanced use cases.

parseHydrationDirective ​

Parses hydration props to determine the strategy.

ts
function parseHydrationDirective(props: HydrationProps): {
  strategy: 'load' | 'idle' | 'visible' | 'media' | 'none'
  media?: string
}
ts
const result = parseHydrationDirective({ 'client:visible': true })
// { strategy: 'visible' }

const result2 = parseHydrationDirective({ 'client:media': '(max-width: 768px)' })
// { strategy: 'media', media: '(max-width: 768px)' }

createHydrationTrigger ​

Creates a trigger that calls the hydration callback based on the strategy.

ts
function createHydrationTrigger(
  strategy: HydrationStrategy,
  element: Element,
  onHydrate: () => void,
  media?: string
): () => void  // Returns cleanup function
ts
const cleanup = createHydrationTrigger(
  'visible',
  document.querySelector('[data-island]')!,
  () => console.log('Hydrating!'),
)

// Clean up observer when done
cleanup()

stripHydrationProps ​

Removes hydration directive props from component props.

ts
function stripHydrationProps<P extends HydrationProps>(
  props: P
): Omit<P, keyof HydrationProps>
ts
const props = { 'client:visible': true, initialCount: 5 }
const cleanProps = stripHydrationProps(props)
// { initialCount: 5 }

shouldHydrate ​

Determines if a component should hydrate based on strategy.

ts
function shouldHydrate(
  strategy: HydrationStrategy,
  media?: string
): boolean | (() => boolean)
ts
shouldHydrate('load')     // true
shouldHydrate('idle')     // false (resolved by idle callback)
shouldHydrate('visible')  // false (resolved by intersection observer)
shouldHydrate('media', '(max-width: 768px)')  // () => window.matchMedia('...').matches

Island ID Utilities ​

ts
// Generate a unique island ID
function generateIslandId(): string
// 'island-1', 'island-2', etc.

// Reset the counter (for testing)
function resetIslandCounter(): void

// Get the current count
function getIslandCount(): number

Released under the MIT License.