Перейти к содержимому

8. React в Astro

Интеграция React в Astro позволяет использовать мощь React-компонентов внутри быстрых статических страниц. Благодаря архитектуре Islands вы получаете лучшее из двух миров: скорость статического HTML и интерактивность React там, где она действительно нужна.

Окно терминала
npx astro add react

Команда автоматически:

  • Устанавливает @astrojs/react, react, react-dom
  • Обновляет astro.config.mjs
  • Настраивает TypeScript типы (при наличии TS)
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
export default defineConfig({
integrations: [react()],
});

Использование React компонентов в .astro файлах

Заголовок раздела «Использование React компонентов в .astro файлах»
---
import Counter from '../components/Counter.jsx';
import LikeButton from '../components/LikeButton.jsx';
---
<html>
<body>
<!-- Статический контент Astro (нет JS) -->
<h1>Мой блог</h1>
<p>Здесь контент без единой строки JS...</p>
<!-- React Islands — добавляют интерактивность -->
<Counter client:load />
<LikeButton client:visible />
</body>
</html>
src/components/Counter.jsx
import { useState } from 'react';
export default function Counter({ initialCount = 0 }) {
const [count, setCount] = useState(initialCount);
return (
<div className="counter">
<button onClick={() => setCount(c => c - 1)}>−</button>
<span>{count}</span>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}

Props передаются как обычные атрибуты. Astro сериализует их в JSON при SSR.

---
import PostActions from '../components/PostActions.jsx';
const post = await getPost(Astro.params.slug);
---
<PostActions
client:load
postId={post.id}
initialLikes={post.likes}
title={post.title}
isBookmarked={post.bookmarked}
/>
src/components/PostActions.jsx
import { useState } from 'react';
export default function PostActions({ postId, initialLikes, title, isBookmarked }) {
const [likes, setLikes] = useState(initialLikes);
const [bookmarked, setBookmarked] = useState(isBookmarked);
const like = () => {
setLikes(l => l + 1);
fetch('/api/like', { method: 'POST', body: JSON.stringify({ postId }) });
};
return (
<div>
<button onClick={like}>❤️ {likes}</button>
<button onClick={() => setBookmarked(b => !b)}>
{bookmarked ? '🔖 Сохранено' : '📌 Сохранить'}
</button>
</div>
);
}

Все React хуки работают как обычно внутри Islands:

src/components/ThemeToggle.jsx
import { useState, useEffect } from 'react';
export default function ThemeToggle() {
const [theme, setTheme] = useState('light');
useEffect(() => {
const saved = localStorage.getItem('theme') || 'light';
setTheme(saved);
document.documentElement.setAttribute('data-theme', saved);
}, []);
const toggle = () => {
const next = theme === 'light' ? 'dark' : 'light';
setTheme(next);
localStorage.setItem('theme', next);
document.documentElement.setAttribute('data-theme', next);
};
return (
<button onClick={toggle} aria-label="Переключить тему">
{theme === 'light' ? '🌙' : '☀️'}
</button>
);
}
---
import Header from '../components/Header.astro'; // Astro — 0 JS
import Sidebar from '../components/Sidebar.astro'; // Astro — 0 JS
import SearchBar from '../components/SearchBar.jsx'; // React Island
import Comments from '../components/Comments.jsx'; // React Island
---
<Layout>
<Header />
<SearchBar client:load />
<main>
<slot />
<Comments client:visible postId={post.id} />
</main>
<Sidebar />
</Layout>

Каждый React Island является независимым и изолированным. Состояние не разделяется между ними автоматически — используйте nanostores или Context через общий стор для коммуникации.

// src/store/cart.js — nanostores для общего стейта
import { atom } from 'nanostores';
export const cartCount = atom(0);
// CartIcon.jsx — реагирует на общий стор
import { useStore } from '@nanostores/react';
import { cartCount } from '../store/cart';
export default function CartIcon() {
const count = useStore(cartCount);
return <span>🛒 {count}</span>;
}