Using React Components in Astro
Dropping a React component into an Astro page and hydrating it as an island.
3 min read
Astro's UI-framework integrations let you write a component in React (or Vue, Svelte, Solid, Preact) and drop it straight into an .astro file. This lesson walks through the React case end to end — adding the integration, writing the component, and hydrating it.
Adding the integration
npx astro add reactThis installs the necessary packages, registers the React integration in astro.config.mjs, and sets up JSX support — after running it, .jsx/.tsx files can be imported into .astro pages.
Writing a React component
There's nothing Astro-specific about the component itself — it's an ordinary React component with its own local state:
// src/components/Counter.jsx
import { useState } from "react";
export default function Counter({ initialCount = 0 }) {
const [count, setCount] = useState(initialCount);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}useState, event handlers, effects — all standard React, because this file is compiled and hydrated by React exactly as it would be in a React-only app. Astro doesn't reinvent how React components work; it controls whether and when React's runtime shows up on the page at all.
Dropping it into a page
---
// src/pages/index.astro
import Counter from "../components/Counter.jsx";
---
<html lang="en">
<body>
<h1>My Astro Site</h1>
<!-- Static, no JavaScript: -->
<Counter initialCount={5} />
<!-- Interactive island: -->
<Counter initialCount={10} client:visible />
</body>
</html>Both usages render the same component, and props pass through exactly as they would in React (initialCount={5}). The difference is entirely the client directive: the first <Counter> renders as static, non-clickable markup with zero JavaScript; the second hydrates once it scrolls into view, becoming a fully working, stateful counter. It's easy to demonstrate this side by side, but in a real page you'd typically only add the directive to the instance that actually needs to respond to clicks.
Passing complex props
Props flow from Astro's frontmatter into the React component just like any other prop, including data fetched or computed server-side:
---
import UserCard from "../components/UserCard.jsx";
const user = { name: "Ada Lovelace", role: "Engineer" };
---
<UserCard user={user} client:load />The user object is computed on the server (or at build time) and serialized into the props React hydrates with — meaning the data-fetching code itself (an API call, a database query) never ships to the browser, only its result. Passing large or deeply nested objects across the hydration boundary does add to the HTML payload (Astro serializes props into the page), so for anything sizable, consider whether the component could fetch its own data client-side instead, or whether it needs the full object at all.
Mixing frameworks on one page
Because each island hydrates independently, an Astro page isn't limited to one UI framework. A React <Counter> and a Svelte <SearchBox> can coexist on the same page, each with its own client directive, neither aware the other exists — a flexibility that's rarely useful to reach for on purpose, but is a natural consequence of how islands are isolated from each other.