Одна из главных суперсил Astro — поддержка React-компонентов как «островов» интерактивности. Ваш статический сайт на Astro может использовать любые React-компоненты и npm-пакеты из React-экосистемы, при этом сохраняя нулевой JavaScript по умолчанию.
Установка
npx astro add react Эта команда автоматически:
- Устанавливает
@astrojs/react,react,react-dom - Обновляет
astro.config.mjs - Добавляет типы TypeScript
// astro.config.mjs (после установки)
import react from '@astrojs/react';
export default defineConfig({
integrations: [react()],
}); Основная концепция: React как остров
Ключевое отличие от чистого React: в Astro React-компонент по умолчанию рендерится только на сервере (HTML). Клиентский JavaScript загружается только по явному указанию через директивы client:*.
---
import StaticHeader from './StaticHeader.astro'; // Astro-компонент — нет JS
import ReactCounter from './ReactCounter.jsx'; // React-компонент
// Данные передаются в React-компонент как пропсы
const initialCount = 0;
---
<!-- Статичный HTML, без JS -->
<StaticHeader />
<!-- Клиентский JS загружается немедленно -->
<ReactCounter count={initialCount} client:load />
<!-- Клиентский JS загружается когда компонент попадает в viewport -->
<ReactCounter count={initialCount} client:visible />
<!-- JS загружается при простое браузера -->
<ReactCounter count={initialCount} client:idle /> Директивы client:*
| Директива | Поведение | Когда использовать |
|---|---|---|
client:load | Загружается немедленно | Критичные элементы (навигация, поиск) |
client:idle | При простое браузера | Некритичные виджеты |
client:visible | Когда входит в viewport | Компоненты ниже fold |
client:media="(max-width: 768px)" | По медиа-запросу | Мобильные меню |
client:only="react" | Только на клиенте (нет SSR) | Компоненты с браузерным API |
Пример React-компонента
// src/components/SearchBox.tsx
import { useState, useEffect } from 'react';
interface Post {
title: string;
url: string;
description: string;
}
interface Props {
posts: Post[];
}
export default function SearchBox({ posts }: Props) {
const [query, setQuery] = useState('');
const [results, setResults] = useState<Post[]>([]);
useEffect(() => {
if (!query.trim()) {
setResults([]);
return;
}
const q = query.toLowerCase();
setResults(
posts.filter(
(p) =>
p.title.toLowerCase().includes(q) || p.description.toLowerCase().includes(q),
),
);
}, [query, posts]);
return (
<div className="search-box">
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Поиск по статьям..."
className="search-input"
/>
{results.length > 0 && (
<ul className="search-results">
{results.map((post) => (
<li key={post.url}>
<a href={post.url}>
<strong>{post.title}</strong>
<span>{post.description}</span>
</a>
</li>
))}
</ul>
)}
</div>
);
} Использование в Astro:
---
import { getCollection } from 'astro:content';
import SearchBox from '../components/SearchBox.tsx';
// Данные собираются на сервере при сборке
const articles = await getCollection('articles');
const posts = articles.map((a) => ({
title: a.data.title,
url: `/articles/${a.id.replace('.mdx', '')}/`,
description: a.data.description,
}));
---
<!-- posts передаётся в React как сериализованный JSON -->
<SearchBox posts={posts} client:visible /> Хуки и состояние
Все React-хуки работают без изменений внутри острова:
import { useState, useCallback, useRef } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
const prevCount = useRef(count);
const increment = useCallback(() => {
prevCount.current = count;
setCount((c) => c + 1);
}, [count]);
return (
<div>
<p>
Текущее: {count}, Прошлое: {prevCount.current}
</p>
<button onClick={increment}>+1</button>
</div>
);
} Передача данных из Astro в React
---
// Простые типы — передаются напрямую
const title = 'Привет, React!';
const count = 42;
const tags = ['Astro', 'React'];
const config = { theme: 'dark', lang: 'ru' };
import MyReactComponent from '../components/MyReactComponent.tsx';
---
<MyReactComponent title={title} count={count} tags={tags} config={config} client:load /> Важно: Пропсы должны быть сериализуемы в JSON. Нельзя передавать функции, классы или замыкания из Astro в React через
client:*директивы.
Astro-слот внутри React
Используйте children для передачи Astro-контента в React-компонент:
---
import Modal from '../components/Modal.tsx';
---
<Modal title="Подтвердите действие" client:load>
<p>Вы уверены, что хотите удалить элемент?</p>
<button>Отмена</button>
</Modal> // Modal.tsx
interface Props {
title: string;
children: React.ReactNode;
}
export default function Modal({ title, children }: Props) {
const [open, setOpen] = React.useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Открыть</button>
{open && (
<div className="modal">
<h2>{title}</h2>
{children}
<button onClick={() => setOpen(false)}>✕</button>
</div>
)}
</>
);
} React + nanostores
Для общего состояния между несколькими React-островами используйте nanostores. Острова подписываются на стор и синхронизируются.
React Query в Astro
@tanstack/react-query работает в React-островах без изменений. Используйте для
загрузки данных на клиенте после гидратации.
Framer Motion
Анимации через Framer Motion работают в React-островах. Используйте client:visible —
анимация запустится когда элемент попадёт в экран.
Context API в островах
Context API работает внутри одного острова, но не между разными островами (они изолированы):
// Правильно: Context внутри одного острова
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext<'light' | 'dark'>('light');
export default function ThemedApp() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
return (
<ThemeContext.Provider value={theme}>
<ThemeToggle onToggle={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))} />
<ThemedContent />
</ThemeContext.Provider>
);
}
function ThemedContent() {
const theme = useContext(ThemeContext);
return <div className={`theme-${theme}`}>...</div>;
} Для состояния между разными островами → используйте nanostores (см. Идеальный стек на Astro).
React-библиотеки, совместимые с Astro
| Библиотека | Назначение | Работает в Astro |
|---|---|---|
@tanstack/react-query | Запросы к API | ✅ |
framer-motion | Анимации | ✅ |
react-hook-form | Формы | ✅ |
zustand | Состояние | ✅ (внутри острова) |
shadcn/ui | UI-компоненты | ✅ |
recharts | Графики | ✅ |
react-map-gl | Карты | ✅ client:only="react" |
next/image | Оптимизация изображений | ❌ (Next.js-специфично) |
Итог
React в Astro — это лучшее из двух миров: статический сайт с идеальным PageSpeed и возможность использовать всю React-экосистему там, где это действительно нужно. Директива client:visible — ваш главный инструмент для ленивой загрузки React-компонентов без потери UX. Используйте React только для интерактивных элементов, остальное — в .astro-файлах.