Astro, Sanity, and React | A Stack Worth Talking About
How we think about fast content-rich web sites without the usual trade-offs, combining the zero-JS default of Astro, the structured content model of Sanity, and React where interactivity is worth the overhead.
Team Skalafy
Contributor
Every content project sooner or later hits a wall where you want the site to load fast, rank well in search engines, update easily for someone who is not a developer, and then also have one interactive feature that actually uses real JavaScript. Search box, Filter, Live form, Something.
Most frameworks fail here. You can go all-in on React and ship a heavy client-side bundle to every page, even pages that are just text and images. Or you can go static and then immediately begin to implement workarounds when a stakeholder requests something dynamic.
Our go-to stack for these situations is Astro, Sanity for content, and React when interactivity is actually needed. Not everywhere. Just when needed. That’s a key distinction.
Astro Ships HTML, Not JavaScript Assumptions
Astro’s default mode of operation is to render everything to static HTML and send zero JavaScript to the browser. That’s not a simplified explanation, that’s literally what happens. Your code runs at build time, it generates HTML, and that’s what the users get.
What changes the game is the Islands Architecture. When you really need the component to be interactive in the browser, you add the client directive to the component. That’s it. That one directive tells Astro: “hydrate this component, give it JavaScript, make it live. Everything else can be static.”
<!-- Static - no JS shipped, just clean HTML -->
<PostList posts={posts} />
<!-- Interactive island - React hydrates only this component -->
<!-- client:visible = only when scrolled into view -->
<SearchBar client:visible posts={posts} />
The result is a page where the blog post content, the header, the footer, and everything else is static HTML, and the search bar is a React component that comes alive when the user scrolls to it. Not before
You’re not opting out of JavaScript. You’re opting in, intentionally, and only where it earns its place.
Sanity Gives Content Editors Room to Breathe
Sanity fits in this category because it doesn’t attempt to own your frontend. Sanity is a structured content platform, and you define how you want your content to be structured in TypeScript, and they build an editor for you, and you receive your data in GROQ queries in exactly the form you asked for.
Your schema lives in your own codebase. If you need to change your content model, you update a file and commit it. Editors receive an interface that matches your actual content model. And GROQ allows you to ask for exactly what you need, rather than over-fetching, rather than client-side filtering of an overly verbose API response.
// Ask for exactly what you need - nothing extra
const POSTS_QUERY = `
*[_type == "post"] | order(publishedAt desc) {
title,
slug,
publishedAt,
author->{ name } // joins the author document inline
}
`;
One thing that’s easy to overlook: Sanity Studio can live inside your Astro project at a /studio route. Editors log in at yoursite.com/studio, make their changes, and the site rebuilds. No separate CMS domain, no separate deployment, no context switching between systems.
React Shows Up When It’s Actually Useful
The point is that since React is not a default runtime in this situation, using it is a decision, not a fact. You write a React component when you actually need state changes, event handlers, browser APIs, etc. When you actually need JavaScript.
All of that just feels natural when you’re working in a different way. The question of whether something needs to be interactive is just part of how you think about your user interface. And when you actually do need something to be interactive, React just works as you’d expect.
// A React component for the parts that need to be alive
export default function LikeButton({ postId, initialCount = 0 }) {
const [count, setCount] = useState(initialCount);
const [liked, setLiked] = useState(false);
return (
<button
onClick={() => {
setCount(liked ? count - 1 : count + 1);
setLiked(!liked);
}}
>
{liked ? "❤️" : "🤍"} {count}
</button>
);
}
Drop it into an Astro page with client:visible, and it ships its JavaScript bundle only to users who scroll past it. Pages that don’t include it don’t pay its cost. That’s the deal.
What This Looks Like in a Real Project
A typical project that uses this tech stack has the following structure:
Sanity manages all the content: posts, authors, images, categories… Astro loads that content at build time and generates static pages. GROQ queries live in one file along with the TypeScript interface definitions, meaning the data flowing from CMS to template is fully typed throughout the entire stack. Environment variables keep credentials out of the codebase.
And then, wherever the design needs actual interactivity-search interface, contact form with validation, dynamic content filter… A React island handles it all. Hydrated with the right directive, costing JavaScript only where the pages and scroll positions warrant it.
The sites that come out of this process load quickly without really trying too hard. They have good scores on Core Web Vitals out of the box. Editors can update these sites without needing a developer. And the interactive parts work exactly as they should, because React is good at that.
It’s not a complicated tech stack. But it’s one that fits together well to solve the brief that comes up most often, and solve it well.
Further Reading
Share this article
Written by Team Skalafy
Contributor at Skalafy
Passionate about building performant, scalable applications with modern technologies. Specializing in Astro.