Astro Islands Architecture Skill
When to Use This Skill
Use this skill when:
- Adding interactive components to Astro pages
- Deciding between React, vanilla JS, or static HTML
- Troubleshooting hydration errors
- Optimizing JavaScript bundle size
Core Principle
React is for composition and layout. Vanilla JS is for interactivity.
In Astro, the best architecture ships minimal JavaScript:
- Use React/components for SSR composition (zero JS shipped)
- Use vanilla JS for simple interactions (theme toggle, mobile menu)
- Use
client:onlysparingly for complex widgets that truly need React state
Astro Client Directives
| Directive | Server Renders? | Ships JS? | Use For |
|---|---|---|---|
| None | Yes | No | Static content, layouts |
client:load |
Yes | Yes | Hydrates immediately (avoid if possible) |
client:idle |
Yes | Yes | Hydrates when idle |
client:visible |
Yes | Yes | Hydrates when visible |
client:only="react" |
No | Yes | Complex widgets, no SSR |
Architecture Patterns
Pattern 1: Pure SSR (Recommended for most content)
---
// index.astro
import { HomePage } from '../components/HomePage';
const data = await fetchData();
---
<HomePage data={data} /> <!-- NO client directive = static HTML -->
What you get:
- React components render to static HTML at build time
- Zero JavaScript shipped
- Fully testable in isolation
- Fast page loads
Pattern 2: Vanilla JS for Simple Interactivity
---
// Layout.astro
---
<button id="theme-toggle">Toggle Theme</button>
<script is:inline>
document.getElementById('theme-toggle').addEventListener('click', () => {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
});
</script>
Use for:
- Theme toggles (~20 lines)
- Mobile menu open/close (~15 lines)
- Simple form validation
- Scroll-triggered animations
Benefits:
- ~0.5kb vs ~5kb+ for React hydration
- No hydration bugs
- Works immediately (no waiting for React to load)
Pattern 3: client:only for Complex Widgets (Last Resort)
---
// page.astro
import { PhotoGallery } from '../components/PhotoGallery';
---
<!-- Only when vanilla JS isn't sufficient -->
<PhotoGallery client:only="react" photos={photos} />
Use for:
- Complex state management
- Heavy user interaction (drag-drop, canvas)
- Third-party React libraries
Trade-offs:
- Ships React runtime (~5kb+)
- Renders empty until JS loads
- Cannot be nested inside SSR React
Critical Constraint
You CANNOT Nest Client Islands Inside SSR React
<!-- THIS DOES NOT WORK -->
<HomePage data={data} /> <!-- SSR React -->
<script>
// Trying to mount React inside a div that HomePage rendered
createRoot(document.getElementById('widget-root')).render(<Widget />);
</script>
Error: @vitejs/plugin-react can't detect preamble
Why: Astro's React integration doesn't support split-brain React:
- Part server-rendered
- Part client-mounted into server-rendered placeholders
Solution: Use vanilla JS for the interaction, or restructure so the interactive part is a sibling, not a child.
Decision Tree
Need to render HTML?
├── Yes → Use React component (no directive)
│ └── Need interactivity?
│ ├── Simple (toggle, menu) → Vanilla JS script
│ └── Complex (gallery, forms) → Separate client:only component
└── No → Why are you here?
Performance Comparison
| Approach | JS Shipped | Time to Interactive |
|---|---|---|
| SSR React (no directive) | 0kb | Instant |
| SSR + Vanilla JS | ~0.5kb | Instant |
| SSR + client:load | ~5kb+ | After React loads |
| client:only | ~5kb+ | After React loads |
Common Pitfalls
DON'T: Use client:load on Layout Components
<!-- Bad: Ships React for static content -->
<Layout client:load>
<Content />
</Layout>
DON'T: Over-engineer Simple Interactions
<!-- Bad: React for a toggle -->
<ThemeToggle client:only="react" />
<!-- Good: Vanilla JS -->
<button id="theme-toggle">Toggle</button>
<script is:inline>/* 10 lines */</script>
DON'T: Mix Astro Components Inside React
// This DOES NOT WORK
export const Page = () => {
return <AstroComponent />; // Can't import .astro into .tsx
};
DO: Keep React Containers Pure SSR
// Good: No useState, no useEffect
export const HomePage: React.FC<Props> = ({ articles }) => {
return (
<>
<Header />
<ArticleGrid articles={articles} />
<Footer />
</>
);
};
Script Directive Reference
| Directive | Bundled? | Deferred? | Use For |
|---|---|---|---|
<script> |
Yes | Yes | Most scripts |
<script is:inline> |
No | No | Theme init (before paint), small scripts |
Use is:inline for:
- Theme initialization (must run before first paint)
- Tiny scripts where bundling overhead isn't worth it
- Scripts that need to run synchronously
Testing Strategy
| What | Where | How |
|---|---|---|
| React components | Ladle/Storybook | Visual regression |
| Full page with interactions | Astro dev server | Playwright integration tests |
| Vanilla JS | Integration tests | Playwright |
You cannot test vanilla JS interactions in component isolation tools like Ladle - they need the full Astro page context.