Astro.js with React Components: Partial Hydration Strategies
If you’ve ever shipped a React app and winced at the JavaScript bundle size, you’re not alone. Modern frameworks have trained us to send mountains of JavaScript to the browser, even when most of the page is static content. Astro.js flips this on its head with a radical idea: ship zero JavaScript by default, and only add interactivity exactly where you need it.
This is the core of Astro’s islands architecture, and once you understand how to wield it with React components, you’ll wonder why we ever did things differently.
In this post, we’ll explore how to integrate React components into an Astro site, master the client directives that control partial hydration, and build pages that are blazing fast without sacrificing the interactivity your users expect.
What Are Islands and Partial Hydration?
Traditional single-page applications hydrate the entire page. Every component, whether it’s a static footer or a dynamic shopping cart, gets JavaScript shipped and executed in the browser. That’s wasteful.
Astro’s islands architecture treats each interactive component as an isolated “island” in a sea of static HTML. The static parts are rendered at build time with zero client-side JavaScript. The interactive parts — your React components — only get hydrated when you explicitly tell Astro to do so.
This is partial hydration in action. Instead of an all-or-nothing approach, you choose which components need JavaScript and when that JavaScript should load.
Think of it like a newspaper: most of the page is static text and images (plain HTML), but there might be an interactive crossword puzzle embedded in one section (a hydrated React island). You wouldn’t print the entire newspaper on an iPad just for one puzzle.
Setting Up React in Astro
Before we dive into hydration strategies, let’s get React working in an Astro project. The setup is refreshingly simple:
# Create a new Astro project
npm create astro@latest my-hybrid-site
cd my-hybrid-site
# Add the React integration
npx astro add react
That’s it. Astro handles the configuration automatically. Now you can drop React components right into your project. Let’s create a simple interactive counter:
// src/components/Counter.jsx
import { useState } from 'react';
export default function Counter({ initialCount = 0 }) {
const [count, setCount] = useState(initialCount);
return (
<div className="counter">
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}
Now here’s the crucial part — how you use this component in an Astro page determines whether any JavaScript gets shipped at all:
---
// src/pages/index.astro
import Counter from '../components/Counter.jsx';
import Header from '../components/Header.jsx';
---
<html lang="en">
<body>
<!-- This renders as static HTML. No JavaScript shipped. -->
<Header title="My Site" />
<!-- This is an interactive island. JavaScript loads on page load. -->
<Counter client:load initialCount={5} />
</body>
</html>
Notice the Header component has no client directive — it renders to static HTML at build time and ships zero JavaScript. The Counter uses client:load, making it a hydrated island. This distinction is the heart of partial hydration.
Mastering the Client Directives
Astro provides several client directives that give you fine-grained control over when your islands become interactive. Choosing the right one can dramatically impact performance.
client:load — Hydrate Immediately
<Counter client:load />
The component hydrates as soon as the page loads. Use this for critical interactivity that users need right away — navigation menus, authentication forms, or above-the-fold interactive elements.
client:idle — Hydrate When the Browser Is Idle
<NewsletterSignup client:idle />
This waits until the browser’s main thread is free, using requestIdleCallback under the hood. It’s perfect for components that are important but don’t need to be interactive the instant the page renders. This is my go-to default for most interactive components.
client:visible — Hydrate When Scrolled Into View
This is where things get really powerful. Why load JavaScript for a component the user hasn’t even seen yet?
---
import CommentSection from '../components/CommentSection.jsx';
import InteractiveChart from '../components/InteractiveChart.jsx';
import ProductConfigurator from '../components/ProductConfigurator.jsx';
---
<main>
<article>
<!-- Lots of static blog content here... -->
<p>A very long article that users will scroll through...</p>
</article>
<!-- These only hydrate when the user scrolls to them -->
<InteractiveChart client:visible data={chartData} />
<section>
<h2>Comments</h2>
<CommentSection client:visible postId="123" />
</section>
<!-- You can even add a rootMargin to hydrate slightly before visible -->
<ProductConfigurator client:visible={{rootMargin: "200px"}} />
</main>
The client:visible directive uses an IntersectionObserver to detect when the component enters the viewport. For a long blog post with an interactive chart or comment section below the fold, this means you’re not paying the JavaScript cost until the user actually needs it. The rootMargin option lets you start hydration slightly before the component scrolls into view, eliminating any perceptible delay.
client:media — Hydrate Based on Media Queries
<!-- Only hydrate on mobile devices -->
<MobileMenu client:media="(max-width: 768px)" />
<!-- Only hydrate if user prefers no reduced motion -->
<AnimatedWidget client:media="(prefers-reduced-motion: no-preference)" />
This is incredibly useful for responsive designs. If a component only makes sense on mobile (like a hamburger menu), desktop users never download its JavaScript. Period.
client:only="react" — Skip Server Rendering Entirely
<MapWidget client:only="react" lat={51.5} lng={-0.1} />
This one’s special. It skips server-side rendering completely and only renders on the client. Use it for components that depend on browser APIs (window, document, canvas) or third-party libraries that don’t support SSR. Note you must specify the framework.
A Real-World Strategy: Putting It All Together
Let’s look at how you’d architect a realistic e-commerce product page using these strategies:
---
// src/pages/product/[slug].astro
import ProductImages from '../components/ProductImages.jsx';
import AddToCart from '../components/AddToCart.jsx';
import SizeSelector from '../components/SizeSelector.jsx';
import ReviewStars from '../components/ReviewStars.jsx';
import ReviewForm from '../components/ReviewForm.jsx';
import RecommendationCarousel from '../components/RecommendationCarousel.jsx';
import ProductMap from '../components/StoreLocatorMap.jsx';
const product = await getProduct(Astro.params.slug);
---
<Layout title={product.name}>
<!-- Static: renders to HTML, zero JS -->
<h1>{product.name}</h1>
<p>{product.description}</p>
<ReviewStars rating={product.averageRating} />
<!-- Critical interactivity: hydrate immediately -->
<ProductImages client:load images={product.images} />
<SizeSelector client:load sizes={product.sizes} />
<AddToCart client:load productId={product.id} />
<!-- Below the fold: hydrate when visible -->
<RecommendationCarousel client:visible products={product.related} />
<ReviewForm client:visible productId={product.id} />
<!-- Browser API dependent: client-only rendering -->
<ProductMap client:only="react" storeLocations={product.stores} />
</Layout>
Notice the hierarchy of decisions:
- Static by default —
ReviewStarsjust displays a rating. No directive needed. client:loadfor above-the-fold interactions users need immediately.client:visiblefor everything below the fold.client:onlyfor components that can’t run on the server.
This page might ship 70-80% less JavaScript than its full React equivalent, while being functionally identical from the user’s perspective.
Conclusion
Astro’s partial hydration approach isn’t just a performance optimization — it’s a fundamental shift in how we think about building for the web. By treating interactive React components as islands in a sea of static content, you get the best of both worlds: the rich component model and ecosystem of React, with the performance characteristics of a static site.
Here’s your action plan for getting started:
- Audit your current React app — identify which components actually need client-side JavaScript and which are purely presentational.
- Start a new Astro project with the React integration and migrate a single page.
- Default to
client:visibleorclient:idle— you’ll be surprised how rarely you needclient:load. - Measure the difference — run Lighthouse before and after. The results will speak for themselves.
The client directives are deceptively simple, but choosing the right one for each component is where the real craft lies. Start shipping less JavaScript. Your users (and their phone batteries) will thank you.