Astro calendar_today 23 апр. 2026 г. schedule 2 мин

Astro + React: Подключение и использование React-компонентов

Как подключить React к Astro.js и использовать React-компоненты как острова. Директивы client:*, хуки, Context API, передача пропсов из Astro в React.

person
Журналист
Автор
Astro + React — архитектура островов

Одна из главных суперсил Astro — поддержка React-компонентов как «островов» интерактивности. Ваш статический сайт на Astro может использовать любые React-компоненты и npm-пакеты из React-экосистемы, при этом сохраняя нулевой JavaScript по умолчанию.

Установка

code
npx astro add react

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

  • Устанавливает @astrojs/react, react, react-dom
  • Обновляет astro.config.mjs
  • Добавляет типы TypeScript
code
// astro.config.mjs (после установки)
import react from '@astrojs/react';

export default defineConfig({
  integrations: [react()],
});

Основная концепция: React как остров

Ключевое отличие от чистого React: в Astro React-компонент по умолчанию рендерится только на сервере (HTML). Клиентский JavaScript загружается только по явному указанию через директивы client:*.

code
---
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-компонента

code
// 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:

code
---
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-хуки работают без изменений внутри острова:

code
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

code
---
// Простые типы — передаются напрямую
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-компонент:

code
---
import Modal from '../components/Modal.tsx';
---

<Modal title="Подтвердите действие" client:load>
  <p>Вы уверены, что хотите удалить элемент?</p>
  <button>Отмена</button>
</Modal>
code
// 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>
      )}
    </>
  );
}
sync

React + nanostores

Для общего состояния между несколькими React-островами используйте nanostores. Острова подписываются на стор и синхронизируются.

cloud_sync

React Query в Astro

@tanstack/react-query работает в React-островах без изменений. Используйте для загрузки данных на клиенте после гидратации.

animation

Framer Motion

Анимации через Framer Motion работают в React-островах. Используйте client:visible — анимация запустится когда элемент попадёт в экран.

Context API в островах

Context API работает внутри одного острова, но не между разными островами (они изолированы):

code
// Правильно: 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/uiUI-компоненты
rechartsГрафики
react-map-glКартыclient:only="react"
next/imageОптимизация изображений❌ (Next.js-специфично)

Итог

React в Astro — это лучшее из двух миров: статический сайт с идеальным PageSpeed и возможность использовать всю React-экосистему там, где это действительно нужно. Директива client:visible — ваш главный инструмент для ленивой загрузки React-компонентов без потери UX. Используйте React только для интерактивных элементов, остальное — в .astro-файлах.

Портрет автора Дмитрий Соколов

Senior Frontend Engineer / Tech Writer

Senior Frontend Engineer с 9-летним опытом. Специализируется на Astro.js и JAMstack.

Комментарии

Загрузка комментариев...

Оставить комментарий

Комментарии проходят модерацию перед публикацией. Правила

Рекомендуем к прочтению