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 advancedcreateIsland()approach, which gives you control over hydration timing (idle, visible, media, etc.). See Islands Architecture for when to use each approach.
Import
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
function registerIslandComponent(
name: string,
component: ComponentType<any>
): voidExample
// 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
function registerIslandComponents(
components: Record<string, ComponentType<any>>
): voidExample
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
function getIslandComponent(name: string): ComponentType<any> | undefinedExample
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
function hydrateIslands(): Promise<void>Example
import { hydrateIslands } from '@ereo/client'
// Hydrate all islands on the page
await hydrateIslands()How It Works
- Finds all elements with
[data-island]attribute - Reads component name from
data-component - Parses props from
data-props(JSON) - Gets hydration strategy from
data-strategy - Creates appropriate hydration trigger based on strategy
- Hydrates with React when trigger fires
initializeIslands
Initializes the islands system. Called automatically by initClient().
Signature
function initializeIslands(): voidExample
// Usually handled by initClient()
import { initializeIslands } from '@ereo/client'
initializeIslands()cleanupIslands
Cleans up all hydrated islands.
Signature
function cleanupIslands(): voidExample
// 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
function createIsland<P extends Record<string, unknown>>(
component: ComponentType<P>,
name: string
): ComponentType<P & HydrationProps>Example
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
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
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
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
// 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)
// 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
// 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.
<Counter client:load />client:idle
Hydrate when the browser is idle (requestIdleCallback).
<Counter client:idle />client:visible
Hydrate when the element enters the viewport (IntersectionObserver with 200px margin).
<Counter client:visible />client:media
Hydrate when a media query matches.
<Counter client:media="(max-width: 768px)" />No directive
Omitting directives means the component is server-rendered only (never hydrated).
<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 propsdata-media— media query (forclient:media)
Advanced Patterns
Conditional Registration
// Only register on client
if (typeof window !== 'undefined') {
registerIslandComponent('Counter', Counter)
}Lazy Loading Islands
// Lazy load island components
const LazyCounter = lazy(() => import('./islands/Counter'))
registerIslandComponent('Counter', LazyCounter)Island with Context
// 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:
// client.ts
function ThemedButtonWrapper(props) {
return (
<ThemeProvider>
<ThemedButton {...props} />
</ThemeProvider>
)
}
registerIslandComponent('ThemedButton', ThemedButtonWrapper)Shared State Between Islands
// 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
// 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:
// 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 12msHydration Utilities
These utilities are used internally but are exported for advanced use cases.
parseHydrationDirective
Parses hydration props to determine the strategy.
function parseHydrationDirective(props: HydrationProps): {
strategy: 'load' | 'idle' | 'visible' | 'media' | 'none'
media?: string
}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.
function createHydrationTrigger(
strategy: HydrationStrategy,
element: Element,
onHydrate: () => void,
media?: string
): () => void // Returns cleanup functionconst cleanup = createHydrationTrigger(
'visible',
document.querySelector('[data-island]')!,
() => console.log('Hydrating!'),
)
// Clean up observer when done
cleanup()stripHydrationProps
Removes hydration directive props from component props.
function stripHydrationProps<P extends HydrationProps>(
props: P
): Omit<P, keyof HydrationProps>const props = { 'client:visible': true, initialCount: 5 }
const cleanProps = stripHydrationProps(props)
// { initialCount: 5 }shouldHydrate
Determines if a component should hydrate based on strategy.
function shouldHydrate(
strategy: HydrationStrategy,
media?: string
): boolean | (() => boolean)shouldHydrate('load') // true
shouldHydrate('idle') // false (resolved by idle callback)
shouldHydrate('visible') // false (resolved by intersection observer)
shouldHydrate('media', '(max-width: 768px)') // () => window.matchMedia('...').matchesIsland ID Utilities
// 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