React Islands
How to use React components inside Astro, when to keep them, and when to convert to .astro.
Setup
npx astro add react
This installs @astrojs/react, react, and react-dom.
Using React components
Import and use React components in .astro files:
---
import Counter from '../components/Counter.jsx';
---
<!-- Static: renders to HTML, no JS shipped -->
<Counter />
<!-- Interactive: hydrates in the browser -->
<Counter client:load />
Without a client:* directive, React components render once on the server and become static HTML. With a directive, they hydrate and become interactive.
Hydration strategies
| Directive | Loads when | Best for |
|---|---|---|
client:load | Page load | Critical interactive elements |
client:idle | Browser idle | Non-critical interactivity |
client:visible | Enters viewport | Below-fold content |
client:media="(max-width: 768px)" | Media query matches | Responsive interactivity |
client:only="react" | Client only, no SSR | Browser-only components |
Passing props and children
Props work as expected:
<SearchBar client:load placeholder="Search articles..." onResults={undefined} />
Children use Astro’s slot mechanism:
<ReactWrapper client:load>
<p>This HTML becomes children in React</p>
</ReactWrapper>
Named slots map to React props:
<ReactCard client:load>
<h2 slot="header">Title</h2>
<p>Body content</p>
</ReactCard>
In the React component, header arrives as props.header and default content as props.children.
When to convert vs keep React
Convert to .astro when the component:
- Has no interactivity (no
useState,useEffect, event handlers) - Only displays data
- Is a layout or structural wrapper
Keep as React when the component:
- Needs client-side state or effects
- Handles user input
- Uses React-specific libraries (React Query, Framer Motion, etc.)
- Would be expensive to rewrite for little benefit
Rule of thumb: if removing hooks leaves you with a function that returns JSX with no event handlers, it should be an Astro component.
Example: converting a static React component
Before (React):
export default function ArticleCard({ title, date, excerpt }) {
return (
<article className="card">
<h2>{title}</h2>
<time>{new Date(date).toLocaleDateString()}</time>
<p>{excerpt}</p>
</article>
);
}
After (Astro):
---
interface Props {
title: string;
date: string;
excerpt: string;
}
const { title, date, excerpt } = Astro.props;
---
<article class="card">
<h2>{title}</h2>
<time>{new Date(date).toLocaleDateString()}</time>
<p>{excerpt}</p>
</article>
<style>
.card { /* scoped styles */ }
</style>
Benefits: zero JS shipped, scoped styles, simpler code.
Sharing state between islands
Islands are isolated. Use Nano Stores for cross-island state:
npm install nanostores @nanostores/react
// src/stores/search.js
import { atom } from 'nanostores';
export const searchQuery = atom('');
// SearchInput.jsx
import { useStore } from '@nanostores/react';
import { searchQuery } from '../stores/search';
export default function SearchInput() {
const $query = useStore(searchQuery);
return <input value={$query} onChange={e => searchQuery.set(e.target.value)} />;
}
// SearchResults.jsx
import { useStore } from '@nanostores/react';
import { searchQuery } from '../stores/search';
export default function SearchResults() {
const $query = useStore(searchQuery);
// filter/display results based on $query
}
Both islands update reactively from the same store, despite being separate React trees.
Nested Astro inside React? No.
React components cannot import Astro components. The nesting goes one way:
- Astro can render React components (with or without hydration)
- React components can only import other React (or JS) components
If you need to compose Astro content inside a React island, pass it as children via slots.
Performance considerations
Each client:* island loads:
- The React runtime (~40KB gzipped for React 19)
- The component code
- Any dependencies
Multiple React islands on the same page share a single React runtime, but each hydrates independently. If you have many small islands, consider whether a single larger island or a vanilla <script> would be more efficient.