React SPA in Astro with TanStack Router
Astro is usually billed as a web framework for content-driven and/or static websites, shipping zero bytes of JavaScript by default. On the other end of the spectrum, there are solutions like Remix and Next.js for highly dynamic and interactive server rendered web applications.
But Astro can do much more than just static sites. If you need to have any amount of client-side interactivity, the reality is that you’re going to need some JavaScript. Astro offers a great middle ground for this with its Islands architecture. This feature allows you to render HTML pages on the server (or at build time) and sprinkle in “islands” of interactivity where needed, with or without the first render pass on the server.
Another approach is to embed a full blown client rendered Single Page Application (SPA) within a sub route tree of your Astro site for a dashboard, admin panel, or some other highly interactive feature. This is where TanStack Router comes in, probably my favorite new React library. React Router is another option, and it (and by extension, Remix) is great and I’ve used it before, but the type-safety and search parameters handling in TanStack Router is just… *chef’s kiss*.
Setup
React
Getting started with TanStack Router in Astro is pretty straightforward. First, you’ll need to add the react integration to your Astro project. I prefer pnpm
, but use whatever package manager you like:
pnpm astro add react
This will automatically install @astrojs/react
, react
, and react-dom
as well as add the necessary configuration to your astro.config.mjs
file. In this file, you’ll want to ensure that the output is set to hybrid
, enabling dynamic routes for the SPA:
// astro.config.mjs
import react from "@astrojs/react";
import { defineConfig } from "astro/config";
// https://astro.build/config
export default defineConfig({
output: "hybrid",
integrations: [react()],
// ... other config
});
TanStack Router
TanStack Router supports file-based routing as well code based routing. Code based routing is more flexible, but file-based routing is generally preferred due to convention and ease of use. To enable file-based route generation, there are two options: @tanstack/router-vite-plugin
and @tanstack/router-cli
. Since Astro is built on Vite, we can use the Vite plugin:
pnpm add @tanstack/react-router @tanstack/router-vite-plugin
// astro.config.mjs
import { TanStackRouterVite } from "@tanstack/router-vite-plugin";
import { defineConfig } from "astro/config";
// https://astro.build/config
export default defineConfig({
// ... other config
vite: {
plugins: [TanStackRouterVite()],
},
});
Next, create a tsr.config.json
file in the root of your project so that TanStack Router knows where to look for your routes. I’ll be putting everything SPA related in the src/dashboard
directory:
// tsr.config.json
{
"routesDirectory": "./src/dashboard/routes",
"generatedRouteTree": "./src/dashboard/routeTree.gen.ts",
"routeFileIgnorePrefix": "-",
"quoteStyle": "single"
}
Follow the TanStack Router Quick Start and file-based routing guide on how to create your routes. One thing not to forget is to set the basepath
in the createRouter
call. Base path /dashboard
corresponds to src/pages/dashboard
in the Astro project.
const router = createRouter({
routeTree,
basepath: "/dashboard",
});
Astro
Rendering a SPA in Astro is the same as rendering any other React component, except that it needs to handle all the requests in the /dashboard
route tree. Create a “catch-all” route src/pages/dashboard/[...app].astro
, but you can name the file whatever you like:
---
// src/pages/dashboard/[...app].astro
import { App } from "~/dashboard/app";
export const prerender = false;
---
<App client:only="react" />
Two things to note here. First, the prerender
export is set to false
to prevent Astro from trying to generate static pages at build time. Second, the client:only
directive needs to be set to react
so Astro knows what framework to use for client-side rendering.
Conclusion
That should be it! You can now build out the react app just like you would for any app built with TanStack Router. If you want to see a working project, check out the example source code, deployed to Cloudflare Pages.