When to Use an Island
The central Astro philosophy is simple: start static, opt into JS only when you need it. This is the opposite of a React SPA where everything is JavaScript until you work to remove it.
The default-static rule
Section titled “The default-static rule”Ask this question for every component on your page:
“Does this component respond to user interaction or change over time in the browser?”
If yes — make it an island with the right client:* directive.
If no — keep it as a .astro component or a React component without a directive (rendered to static HTML).
// In a React/Next.js app everything is a component.// Making something "static" requires extra effort.export default function BlogPage({ post, related }) { return ( <div> {/* These are all in the React tree, all hydrated */} <Header /> {/* purely presentational */} <ArticleContent post={post} /> {/* purely presentational */} <LikeButton postId={post.id} /> {/* interactive */} <ShareMenu url={post.url} /> {/* interactive */} <RelatedPosts posts={related} />{/* purely presentational */} </div> );}---// In Astro, ask "does this need JS?" for each piece.import Header from '../components/Header.astro'; // no JSimport ArticleContent from '../components/ArticleContent.astro'; // no JSimport LikeButton from '../components/LikeButton.jsx'; // needs JSimport ShareMenu from '../components/ShareMenu.jsx'; // needs JSimport RelatedPosts from '../components/RelatedPosts.astro'; // no JSconst { post, related } = Astro.props;---<Header /><ArticleContent post={post} /><LikeButton client:visible postId={post.id} /><ShareMenu client:idle url={post.url} /><RelatedPosts posts={related} />On a typical blog post page, two islands (LikeButton and ShareMenu) ship JavaScript. Everything else is zero-JS HTML.
Islands are isolated
Section titled “Islands are isolated”This is the most important architectural difference from a React SPA. In a React app there is one component tree. React Context flows from a top-level Provider down to any descendant, no matter how deep.
In Astro, each island is a separate React root. Context does not cross island boundaries.
// This does NOT work across islands://// .astro page:// <ThemeProvider client:load> ← island A// <ThemedButton client:load /> ← island B (separate root!)//// ThemeProvider's context is not visible to ThemedButton.Working around isolation
Section titled “Working around isolation”Option 1 — Wrap both in a single island:
---import ThemedApp from '../components/ThemedApp.jsx';---<ThemedApp client:load />// ThemedApp.jsx — one island, one React root, context worksimport { ThemeProvider } from './ThemeContext';import ThemedButton from './ThemedButton';
export default function ThemedApp() { return ( <ThemeProvider> <ThemedButton /> </ThemeProvider> );}Option 2 — Use a shared signal/store outside React:
Libraries like nanostores (Astro’s recommended tiny store) let islands share state without a common React root:
import { atom } from 'nanostores';export const theme = atom('light');// island Aimport { useStore } from '@nanostores/react';import { theme } from '../stores/theme';export default function ThemeToggle() { const t = useStore(theme); return <button onClick={() => theme.set(t === 'light' ? 'dark' : 'light')}>{t}</button>;}// island B — reads the same storeimport { useStore } from '@nanostores/react';import { theme } from '../stores/theme';export default function ThemedButton() { const t = useStore(theme); return <button class={t}>Click me</button>;}The decision checklist
Section titled “The decision checklist”| Component | Needs hydration? | What to use |
|---|---|---|
| Navigation header | No (links only) | .astro component |
| Hero image | No | <img> in .astro |
| Article body | No | .astro component |
| Like / upvote button | Yes — click handler | React island client:visible |
| Comments section | Yes — fetch + render | React island client:visible |
| Mobile menu toggle | Yes — open/close state | React island client:media |
| Page layout | No | .astro layout |
| Date formatter | No (build-time) | .astro or static React |