Skip to main content
Technology & EngineeringAstro334 lines

Astro Integrations

Using React, Vue, Svelte, and other UI framework islands within Astro pages

Quick Summary33 lines
You are an expert in integrating React, Vue, Svelte, and other UI frameworks as interactive islands within Astro sites.

## Key Points

- Use `client:visible` for below-the-fold components and `client:idle` for non-critical above-the-fold components to minimize initial JS payload.
- Keep island boundaries at the right level: one large island is better than twenty tiny ones that all need to share state.
- Use nanostores (or custom events / localStorage) for cross-island state instead of trying to lift state into Astro.
- Prefer Astro components for anything that does not need client-side interactivity. A static `.astro` card is cheaper than a React card without a client directive.
- When mixing frameworks, be deliberate about which framework handles which concern. Do not use React and Vue for the same type of component on the same site.
- **Missing client directive**: Without `client:*`, a React/Vue/Svelte component renders as static HTML. Event handlers, state, and effects will not work.
- **Passing non-serializable props**: Functions, Dates, Maps, Sets, and class instances cannot be passed as props to hydrated islands. Convert them to serializable formats first.
- **CSS-in-JS overhead**: Libraries like styled-components or Emotion add JavaScript to the client bundle. Prefer scoped Astro styles or CSS Modules for framework components used as islands.
- **Conflicting JSX renderers**: Using both React and Preact without `include` patterns causes ambiguity. Astro does not know which renderer to use for `.jsx` files.
- **Expecting context/providers across islands**: React Context, Vue provide/inject, and Svelte context do not work across separate islands. Each island has its own component tree.

## Quick Example

```bash
npx astro add react
npx astro add vue
npx astro add svelte
npx astro add solid
npx astro add preact
```

```astro
---
import SearchBox from '../components/SearchBox.jsx';
---
<SearchBox client:idle placeholder="Search articles..." />
```
skilldb get astro-skills/Astro IntegrationsFull skill: 334 lines
Paste into your CLAUDE.md or agent config

Framework Integrations — Astro

You are an expert in integrating React, Vue, Svelte, and other UI frameworks as interactive islands within Astro sites.

Overview

Astro's integration system lets you use components from React, Vue, Svelte, Solid, Preact, Lit, and Alpine inside .astro pages. Each framework component becomes an island that hydrates independently. You can even mix frameworks on the same page.

Core Concepts

Adding Integrations

Use the Astro CLI to add framework support:

npx astro add react
npx astro add vue
npx astro add svelte
npx astro add solid
npx astro add preact

This installs the necessary packages and updates astro.config.mjs:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import vue from '@astrojs/vue';
import svelte from '@astrojs/svelte';

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

Using Framework Components

Import and use framework components inside .astro files with a client directive:

---
import ReactCounter from '../components/Counter.jsx';
import VueAccordion from '../components/Accordion.vue';
import SvelteToggle from '../components/Toggle.svelte';
---

<h1>Mixed Framework Page</h1>

<!-- Each component hydrates independently -->
<ReactCounter client:load initialCount={0} />
<VueAccordion client:visible title="FAQ" />
<SvelteToggle client:idle label="Dark Mode" />

Client Directives in Depth

Without a client:* directive, framework components render to static HTML and ship zero JavaScript:

<!-- Static HTML only, no interactivity -->
<ReactCounter initialCount={0} />

<!-- Interactive, hydrates immediately -->
<ReactCounter client:load initialCount={0} />

<!-- Interactive, hydrates when visible in viewport -->
<ReactCounter client:visible initialCount={0} />

<!-- Interactive, hydrates when browser is idle -->
<ReactCounter client:idle initialCount={0} />

<!-- Interactive, hydrates when media query matches -->
<ReactCounter client:media="(min-width: 768px)" initialCount={0} />

<!-- Client-only, skips SSR entirely -->
<ReactCounter client:only="react" initialCount={0} />

Passing Data Between Astro and Framework Components

Astro passes props as serializable data. You can pass strings, numbers, booleans, arrays, and plain objects:

---
const user = { name: 'Alice', role: 'admin' };
const items = ['apple', 'banana', 'cherry'];
---

<ReactUserCard client:load user={user} items={items} editable={true} />

You cannot pass functions, class instances, or non-serializable values as props to hydrated components. The data crosses a serialization boundary.

Implementation Patterns

React Components with Hooks

// src/components/SearchBox.jsx
import { useState, useEffect } from 'react';

export default function SearchBox({ placeholder = 'Search...' }) {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  useEffect(() => {
    if (query.length < 2) {
      setResults([]);
      return;
    }

    const controller = new AbortController();
    fetch(`/api/search?q=${encodeURIComponent(query)}`, {
      signal: controller.signal,
    })
      .then(r => r.json())
      .then(setResults)
      .catch(() => {});

    return () => controller.abort();
  }, [query]);

  return (
    <div className="search-box">
      <input
        type="text"
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder={placeholder}
      />
      <ul>
        {results.map(r => (
          <li key={r.id}>{r.title}</li>
        ))}
      </ul>
    </div>
  );
}
---
import SearchBox from '../components/SearchBox.jsx';
---
<SearchBox client:idle placeholder="Search articles..." />

Vue Single-File Components

<!-- src/components/Accordion.vue -->
<template>
  <div class="accordion">
    <button @click="isOpen = !isOpen" class="accordion-trigger">
      {{ title }}
      <span>{{ isOpen ? '−' : '+' }}</span>
    </button>
    <div v-show="isOpen" class="accordion-content">
      <slot />
      <p>{{ content }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const props = defineProps({
  title: { type: String, required: true },
  content: { type: String, default: '' },
});

const isOpen = ref(false);
</script>

Svelte Components

<!-- src/components/Toggle.svelte -->
<script>
  export let label = 'Toggle';
  export let initial = false;

  let active = initial;

  function toggle() {
    active = !active;
    document.documentElement.classList.toggle('dark', active);
  }
</script>

<button on:click={toggle} class:active>
  {label}: {active ? 'On' : 'Off'}
</button>

<style>
  button {
    padding: 0.5rem 1rem;
    border-radius: 4px;
    cursor: pointer;
  }
  .active {
    background: #333;
    color: #fff;
  }
</style>

Nested Framework Components

Framework components can import other components from the same framework:

// src/components/Dashboard.jsx
import Chart from './Chart.jsx';
import StatCard from './StatCard.jsx';

export default function Dashboard({ stats, chartData }) {
  return (
    <div className="dashboard">
      <div className="stats">
        {stats.map(s => <StatCard key={s.id} {...s} />)}
      </div>
      <Chart data={chartData} />
    </div>
  );
}
---
import Dashboard from '../components/Dashboard.jsx';
const stats = await fetch('/api/stats').then(r => r.json());
const chartData = await fetch('/api/chart').then(r => r.json());
---

<!-- Single island for the whole dashboard -->
<Dashboard client:load stats={stats} chartData={chartData} />

Sharing State Between Islands

Islands are isolated by default. Share state using browser-native mechanisms:

// src/lib/store.ts (using nanostores — works across frameworks)
import { atom, map } from 'nanostores';

export const cartCount = atom(0);
export const cartItems = map<Record<string, { name: string; qty: number }>>({});

export function addToCart(id: string, name: string) {
  const current = cartItems.get()[id];
  cartItems.setKey(id, { name, qty: (current?.qty ?? 0) + 1 });
  cartCount.set(Object.values(cartItems.get()).reduce((sum, i) => sum + i.qty, 0));
}
// React component using the shared store
import { useStore } from '@nanostores/react';
import { cartCount, addToCart } from '../lib/store';

export default function AddToCartButton({ productId, productName }) {
  const count = useStore(cartCount);
  return (
    <button onClick={() => addToCart(productId, productName)}>
      Add to Cart ({count})
    </button>
  );
}

Configuring Multiple Frameworks

When using React alongside Preact or other JSX frameworks, specify which files each handles:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import preact from '@astrojs/preact';

export default defineConfig({
  integrations: [
    react({ include: ['**/react-components/*'] }),
    preact({ include: ['**/preact-components/*'] }),
  ],
});

Core Philosophy

Astro's integration system treats UI frameworks as tools rather than foundations. Instead of building your entire site in React or Vue, you use these frameworks surgically for components that genuinely need client-side state, event handlers, or lifecycle effects. This reversal of the typical framework relationship means the framework serves the site, not the other way around.

The island model enforces a natural boundary between server-rendered content and client-interactive components. Each island is independently hydrated, independently bundled, and independently cached. This isolation is not a limitation but a feature: it prevents one slow or broken island from affecting the rest of the page, and it forces you to design components with clear inputs (serializable props) and self-contained behavior.

Mixing frameworks on a single page is technically possible but philosophically significant. Astro does not want you to use React for a counter and Vue for a dropdown on the same page. The multi-framework capability exists to support gradual migrations and team specialization, not to encourage framework shopping within a single project. When you commit to multiple frameworks, do so with clear organizational boundaries.

Anti-Patterns

  • Using framework components for static content. Rendering a card, a header, or a footer in React without a client:* directive produces the same HTML as an Astro component but adds bundler overhead. Default to .astro for anything without interactivity.

  • Passing functions or class instances as island props. Island props cross a serialization boundary. Functions, Dates, Maps, Sets, and class instances silently break or get stripped. Convert everything to plain JSON-compatible data before passing it to a hydrated component.

  • Creating many tiny islands that share state. Twenty small React islands that all need to know about a shared cart or user session create synchronization headaches. Group related interactive elements into a single island so they share a component tree and state naturally.

  • Using CSS-in-JS libraries inside islands. Libraries like styled-components or Emotion add JavaScript weight to each island's bundle. Use scoped Astro styles, CSS Modules, or Tailwind instead, keeping the island focused on behavior rather than styling.

  • Mixing multiple frameworks without clear boundaries. Using React and Svelte in the same feature area creates maintenance confusion about which framework owns which concern. If you mix frameworks, assign each one a distinct responsibility or section of the application.

Best Practices

  • Use client:visible for below-the-fold components and client:idle for non-critical above-the-fold components to minimize initial JS payload.
  • Keep island boundaries at the right level: one large island is better than twenty tiny ones that all need to share state.
  • Use nanostores (or custom events / localStorage) for cross-island state instead of trying to lift state into Astro.
  • Prefer Astro components for anything that does not need client-side interactivity. A static .astro card is cheaper than a React card without a client directive.
  • When mixing frameworks, be deliberate about which framework handles which concern. Do not use React and Vue for the same type of component on the same site.

Common Pitfalls

  • Missing client directive: Without client:*, a React/Vue/Svelte component renders as static HTML. Event handlers, state, and effects will not work.
  • Passing non-serializable props: Functions, Dates, Maps, Sets, and class instances cannot be passed as props to hydrated islands. Convert them to serializable formats first.
  • CSS-in-JS overhead: Libraries like styled-components or Emotion add JavaScript to the client bundle. Prefer scoped Astro styles or CSS Modules for framework components used as islands.
  • Conflicting JSX renderers: Using both React and Preact without include patterns causes ambiguity. Astro does not know which renderer to use for .jsx files.
  • Expecting context/providers across islands: React Context, Vue provide/inject, and Svelte context do not work across separate islands. Each island has its own component tree.

Install this skill directly: skilldb add astro-skills

Get CLI access →