Published on January 5, 2024

Build a Single Page Application (SPA) with Astro

Build a Single Page Application (SPA) with Astro

Building a Single Page Application (SPA) with Astro may sound like a strange idea at first. I mean, Astro is mostly advertised for building content-driven Multi-Page Applications (MPAs) and static websites, with a focus on delivering high performance by shipping less JavaScript. So why would you want to build an SPA with Astro?

Well, the answer is simple: You can build a SPA with Astro next to your static and server-rendered pages. This way, you can have the best of both worlds: a fast and SEO-friendly website for your marketing pages, and an SPA for your interactive application.

At the same time, you can also use Astro to build your backend API. This way, you can build your entire application with Astro, without having to use any other framework or tool.

From there, you can do all sorts of things: bring in tRPC and React Query to handle the client-server communication, re-use your React components on the server side, and much more.

Setting up Astro

To get started, let's create a new Astro project and add React and the Node.js adapter:

npm create astro@latest

Change into the newly created directory and install React and the Node.js adapter:

npx astro add react
npx astro add node

Next, install React Router:

npm install react-router-dom

With that done, we can start building our application.

Building our Single Page Application (SPA)

Let's start by creating a new file called src/components/app.tsx and create a very simple React application using React Router.

First, let's create a few pages:

const Dashboard = () => <h1>Dashboard</h1>;
const Settings = () => <h1>Settings</h1>;
const Profile = () => <h1>Profile</h1>;

Next, let's create a simple nav bar using React Router's Link component:

import { Link } from 'react-router-dom';

const Navbar = () => {
return (
<nav>
<ul>
<li>
<Link to="/dashboard">Dashboard</Link>
</li>
<li>
<Link to="/dashboard/settings">Settings</Link>
</li>
<li>
<Link to="/dashboard/profile">Profile</Link>
</li>
</ul>
</nav>
);
};

Then, let's create a simple layout component that renders the navbar and the current page:

import { Outlet } from 'react-router-dom';

const Layout = () => (
<div>
<Navbar />
<div>
<Outlet />
</div>
</div>
);

Finally, let's define our router using createBrowserRouter and render our application:

import { createBrowserRouter, RouterProvider } from 'react-router-dom';

const router = createBrowserRouter([
{
path: 'dashboard',
element: <Layout />,
children: [
{ path: '', element: <Dashboard /> },
{ path: 'settings', element: <Settings /> },
{ path: 'profile', element: <Profile /> }
]
}
]);

export const App = () => {
return (
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
};

With that done, we now have a very simple React application with three pages and a navigation bar. Let's now move on to the Astro part.

Adding a route for our SPA

To add a route for our SPA, we need to create a new file and render our React app just like we would do with any other React component when using Astro. The only difference is that we need to make sure we set up a catch-all route so that all requests are handled by our SPA.

To do that, let's create a new file called src/pages/dashboard/[...all].astro. This will create a new route for our SPA that matches all routes starting with /dashboard. You can, of course, change this to whatever you want.

Next, we want to import our Astro's Layout component and render our React app inside of it:

---
import { App } from "../../components/app";
import Layout from "../../layouts/Layout.astro";
---

<Layout title="Dashboard">
<App client:only />
</Layout>

Note that we're passing client:only to our React app. This is because we don't want to render our React app on the server side. Instead, we want to render it on the client side only.

Now that we have our SPA route set up, we can run our application and see it in action:

npm run dev

Head over to http://localhost:4321 and you should see your Astro application. If you navigate to http://localhost:4321/dashboard, you should see your React application and be able to navigate between the different pages.

Wrapping up

As you can see, it's very easy to build an SPA with Astro. All we did was create a catch-all route in our Astro application and let it render our React application. You can do the same with any other framework or library, such as Vue, Svelte, or Solid.

From there, what I like to do is use tRPC and React Query to handle data fetching and client-server communication. This way, you get type-safe communication between your client and server, and you could still use the same tRPC endpoints on the server side to fetch data for your static pages.

This setup works well for small to medium-sized applications and provides a great developer experience and performance without having to use a monorepo or any other complex setup.

If you're interested in learning more about Astro, I've written a few other articles on things such as implementing Incremental Static Regeneration (ISR) with Astro and setting up Astro with SQLite and Litestream.

Until next time, happy coding!

Interested in LogSnag?

Get started right now for free!