Technical Reference
React Islands Architecture
How Astro's islands architecture enables selective hydration for optimal performance.
What are islands
Islands are independent, interactive components that load JavaScript only where needed. The rest of the page remains static HTML, reducing bundle size and improving performance.
Key concepts
- • Static by default - Astro components render to HTML with zero JavaScript
- • Selective hydration - Only interactive components load client JavaScript
- • Framework agnostic - Use React, Vue, Svelte, or mix frameworks
- • Partial hydration - Each island hydrates independently
Client directives
Client directives control when and how React components hydrate on the client.
client:load High Priority Hydrate immediately on page load. Use for above-the-fold interactive elements critical to initial page experience.
---
import ChatInterface from '@/components/ChatInterface';
---
<ChatInterface client:load /> When to use: Critical interactive UI, chat interfaces, real-time features
client:idle Medium Priority
Hydrate when browser is idle (uses requestIdleCallback). Good default for most interactive components.
---
import SearchWidget from '@/components/SearchWidget';
---
<SearchWidget client:idle /> When to use: Forms, search, navigation menus, secondary features
client:visible Low Priority
Hydrate when component enters viewport (uses IntersectionObserver). Ideal for below-the-fold content.
---
import ImageCarousel from '@/components/ImageCarousel';
---
<ImageCarousel client:visible /> When to use: Carousels, charts, animations below fold, lazy-loaded widgets
client:media Conditional Hydrate when CSS media query matches. Perfect for responsive components that only work on specific screen sizes.
---
import MobileMenu from '@/components/MobileMenu';
import DesktopMenu from '@/components/DesktopMenu';
---
<MobileMenu client:media="(max-width: 768px)" />
<DesktopMenu client:media="(min-width: 769px)" /> When to use: Mobile-only or desktop-only features
client:only="react" Client Only Skip server rendering entirely. Component only renders on client. Use when server rendering causes issues.
---
import BrowserOnlyWidget from '@/components/BrowserOnlyWidget';
---
<BrowserOnlyWidget client:only="react" /> When to use: Components that depend on browser APIs, third-party widgets, experimental features
Directive decision matrix
Choose the right directive based on your component's purpose and location.
| If your component... | Use | Reason |
|---|---|---|
| Needs immediate interaction | client:load | Critical path, above fold |
| Is a form or search | client:idle | Important but not critical |
| Is below the fold | client:visible | Lazy load for performance |
| Only works on mobile/desktop | client:media | Avoid loading unused code |
| Breaks during SSR | client:only | Skip server rendering |
Creating React islands
Build React components optimized for island architecture.
Basic React island
// src/components/Counter.tsx
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div className="p-4 border rounded-lg">
<p className="mb-2">Count: {count}</p>
<button
onClick={() => setCount(count + 1)}
className="px-4 py-2 bg-craft-accent text-white rounded"
>
Increment
</button>
</div>
);
} Using the island in Astro
---
// src/pages/example.astro
import Counter from '@/components/Counter';
---
<html>
<body>
<h1>Static Header</h1>
<p>Static paragraph with no JavaScript</p>
<!-- Only this component hydrates -->
<Counter client:idle />
<footer>Static footer</footer>
</body>
</html> Island with props
// src/components/Greeting.tsx
interface GreetingProps {
name: string;
initialCount?: number;
}
export default function Greeting({ name, initialCount = 0 }: GreetingProps) {
const [count, setCount] = useState(initialCount);
return (
<div>
<p>Hello, {name}!</p>
<p>Visits: {count}</p>
<button onClick={() => setCount(count + 1)}>Visit Again</button>
</div>
);
}
// In Astro:
// <Greeting client:load name="Ryan" initialCount={5} /> State management patterns
Islands are isolated by default. Share state between islands using these patterns.
Local storage (simple)
// src/components/ThemeToggle.tsx
import { useState, useEffect } from 'react';
export default function ThemeToggle() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
const saved = localStorage.getItem('theme');
if (saved) setTheme(saved as 'light' | 'dark');
}, []);
const toggle = () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
document.documentElement.classList.toggle('dark');
};
return <button onClick={toggle}>Toggle Theme</button>;
} Nano Stores (recommended)
// src/stores/user.ts
import { atom } from 'nanostores';
export const userStore = atom({ name: '', loggedIn: false });
// In any React island:
import { useStore } from '@nanostores/react';
import { userStore } from '@/stores/user';
export default function UserGreeting() {
const user = useStore(userStore);
return <p>Welcome, {user.name}!</p>;
} Custom events (cross-framework)
// Dispatch from one island
const event = new CustomEvent('user-login', {
detail: { userId: 123 }
});
window.dispatchEvent(event);
// Listen in another island
useEffect(() => {
const handler = (e: CustomEvent) => {
console.log('User logged in:', e.detail.userId);
};
window.addEventListener('user-login', handler as EventListener);
return () => window.removeEventListener('user-login', handler as EventListener);
}, []); Performance considerations
Optimize island performance with these best practices.
Minimize island count
Each island adds overhead. Combine related interactive elements into single islands.
<!-- Instead of multiple islands: -->
<LikeButton client:idle />
<ShareButton client:idle />
<CommentButton client:idle />
<!-- Combine into one: -->
<SocialActions client:idle /> Choose appropriate directives
Use client:visible for below-the-fold content. Avoid client:load unless critical.
Code splitting
Heavy dependencies should be dynamically imported.
import { useState } from 'react';
export default function ChartWidget() {
const [Chart, setChart] = useState(null);
useEffect(() => {
// Load chart library only when component mounts
import('chart.js').then((module) => setChart(module.default));
}, []);
return Chart ? <Chart data={data} /> : <p>Loading...</p>;
} Avoid prop drilling
Use Nano Stores or Context for deeply nested state instead of passing props through multiple levels.
Debugging islands
Diagnose island hydration and rendering issues.
Check hydration in DevTools
// Add data-astro-cid attribute to identify islands
<div data-island="ChatInterface">
<!-- Island content -->
</div>
// Check console for hydration errors
// Look for React warnings about mismatched HTML Common issues
| Issue | Solution |
|---|---|
| Island doesn't hydrate | Check client: directive is present |
| Hydration mismatch error | Ensure server and client HTML match exactly |
| Props not passing correctly | Verify props are serializable (no functions) |
| Window/document undefined | Use useEffect or client:only |
Enable Astro Dev Toolbar
// astro.config.mjs
export default defineConfig({
devToolbar: { enabled: true }
});
// View island boundaries and hydration status in browser Best practices summary
- Use
client:idleas default for most interactive components - Prefer
client:visiblefor below-the-fold content - Reserve
client:loadfor critical, above-the-fold interactions - Combine related interactive elements into single islands
- Use Nano Stores for shared state across islands
- Dynamically import heavy dependencies
- Test hydration in production builds, not just dev mode
- Avoid
windowordocumentin component body (useuseEffect)