Why We Switched from React to Astro
Written by Thomas (with help from Claude)
We just shipped a full rewrite of the AgentPatch frontend. The app used to be a React single-page application built with Vite and React Router. Now it’s an Astro site with React islands for the interactive bits. Here’s what drove the change and what we learned.
The problem: two rendering systems
When we first built AgentPatch, we reached for the obvious stack: React + Vite + React Router. It works, it’s fast to develop with, and the ecosystem is massive.
But we also needed static content. Blog posts, documentation, legal pages. A React SPA doesn’t give you real HTML for crawlers, so we wrote a Node script that compiled our markdown into standalone HTML files at build time. That script grew to nearly a thousand lines. It had its own nav template, its own footer template, its own CSS injection, its own pagination logic.
We ended up maintaining two separate rendering systems that produced the same website. Every time we updated the nav, we had to update it in two places. Every time we changed the footer links, two places. Every time we adjusted the layout, two places.
This is the kind of thing that feels manageable at first and slowly drives you insane.
The auth flash
React SPAs have a well-known problem with authenticated navigation. The page loads, the nav renders in a logged-out state, then an async call checks auth, and the nav snaps to logged-in. It’s a fraction of a second, but you notice it. Every page load, a little flicker.
We tried the usual workarounds. Loading spinners. Skeleton states. They all felt like band-aids over an architectural gap: the nav shouldn’t need JavaScript to know whether you’re logged in.
The SEO blind spot
Every interactive page in a React SPA is invisible to search engines unless you add server-side rendering. Our blog and docs were fine (generated as static HTML), but the landing page, the browse page, and individual tool pages were all client-rendered. Googlebot saw an empty <div id="root"></div>.
For a marketplace where discoverability matters, that’s a real problem.
What Astro gave us
Astro’s model is simple: pages are static HTML by default, and you opt into interactivity where you need it using islands. A blog post is zero JavaScript. A login form is a React component that hydrates on the client. The nav and footer are Astro components, no framework runtime needed.
This gave us exactly what we wanted:
One nav, one footer, everywhere. The nav is a single Astro component used across every page. Static pages get it at build time. Interactive pages get it at build time too. There is no second implementation to keep in sync.
No auth flash. We solved the flicker with a tiny inline script that runs before paint. It reads a flag from localStorage synchronously and sets a CSS class on the document root. The nav uses pure CSS to show the right state:
.nav-auth-in { display: none; }
.logged-in .nav-auth-out { display: none; }
.logged-in .nav-auth-in { display: contents; }
No React. No async call. No flicker. The nav shows the correct state instantly, and React hydrates the interactive pages in the background.
Blog and docs for free. Astro has built-in content collections that handle markdown with frontmatter validation, pagination, and automatic sitemap generation. Our thousand-line build script is gone. The blog is now a handful of Astro templates that we can actually read.
Ship less JavaScript. A blog post page loads zero JavaScript. A docs page loads zero JavaScript. The only pages that ship a React bundle are the ones that actually need interactivity: login, browse, tool management, the dashboard. Everything else is plain HTML and CSS.
What stayed the same
All of our React components survived the migration. The login form, the tool editor, the admin panel, the dashboard: they’re all still React. We just moved them from being the entire page to being islands within Astro pages. shadcn/ui, our hooks, our API client, our auth provider, all untouched.
The backend didn’t change at all. Same Cloudflare Workers API, same D1 database, same Hono routes. The migration was purely a frontend concern.
The numbers
Our blog index page went from shipping ~180KB of JavaScript (React + React Router + the entire SPA bundle) to shipping 0KB. The page is static HTML. It loads in a single request.
Interactive pages still load React, but only the components they need. The browse page loads the browse island. The login page loads the login island. No page loads the entire application.
Build output went from a single index.html with a JS bundle to individual HTML files for every route. The blog alone generates 100+ standalone pages, each with proper <title> tags, Open Graph meta, and structured data that search engines can actually read.
Would we do it again?
Without hesitation. Astro’s islands architecture matches how the web actually works: most pages are documents with a few interactive regions, not full applications. Fighting that reality with a React SPA was creating problems we didn’t need to have.
If you’re building a product that has both static content and interactive features, and you find yourself maintaining parallel rendering systems or working around SPA limitations, take a look at Astro. It might save you from the same headaches we had.
AgentPatch is an open marketplace where AI agents discover, purchase, and use tools at runtime. Browse tools or read the docs to get started.