Astro.js as an alternative to Next.js: pushing the limits
Tags: astro, nextjs, react, javascript
Published: 2024-04-17 10:02:29

I’m tired of Next. I want to try something fresh. So I thought this was the time I look into other frameworks. Where would I go? Nuxt? Remix? Nah…

Astro! Let’s test it and see if it can handle modern project requirements.

Here is [the repo](https://github.com/snikidev/astro-limitless), follow along!

## Why am I doing this?

1. Next.js is complicated. But some of the best websites out there use a plain, vanilla JS. We don’t need anything fancy to build great products. I mean, we could use a bit of help from a simple (!!) framework that gets out of your way and lets you deliver. And I want to find that framework.
2. Next.js v13 was a ~~shit~~ shift. In v13 Next.js introduced a mentality shift with RSC on how we develop an app, and since I have to shift my mentality, I’d like to shift it in all possible directions. Open my mind a little bit more than I have to.
3. Vercel is not your friend. And Next.js is THEIR product. Sooner or later I feel like we’ll be shifting our mentality to adjust to how Vercel wants us to do things. I don’t wish to be locked in like that.
4. I’m just tired of Next.js. Every company makes me work with Next.js. Always Next. I don’t really mind, but I miss those times when I was talking about Next as a “new framework” that we “should look into adopting”! I and others like me did a very good job. Well done, folks! But now I want to mix things up a bit, time to try something fresh! Again! 😅

So I guess you could say that Next.js is the reason I’m looking into Astro. 😁

## Enter Astro.js

I’ve been ignoring it like I do with Remix and Svelte and a bunch of other frameworks. However, I’ve learned that Astro is different, and I’m gonna talk about how it’s different later in the post.

First, I needed to define a set of criteria with which I could judge its usefulness. How do I identify whether I should choose it (or recommend it) for a new project?

So I started thinking and reflecting on all the companies and all the projects I ever I worked with. What was I doing there? What requirements they had in common? What kind of work they were assigning to me?

And so I defined a list of such things, things that I was doing on EVERY job or contract. This is what everyone wanted me to do from tiny startups to humongous enterprises.

- Has React & TypeScript: because I’ve rarely seen a company that wants Vue or plain JavaScript. Everyone wants React and it has to be with TypeScript.
- Static pages: Fetch content from headless CMS at build time, create the HTML page and host it somewhere on a simple host.
- Global state management shared between components without prop drilling: Because there’s always at least a menu of some sort that you need to control from 3 different places.
- Custom theme (global): everyone has its own colours, fonts, etc. No hardcoding and repeating/copy+pasting those HEX values all over the CSS files!
- Scoped CSS (e.g. `.module.css`): no one wants to load ALL of the CSS for every page and then use 1/100th of it.
- SSR rendered content that needs to be changed or updated from the client (e.g. using `@tanstack/query`), e.g. you know that sidebar with filters on a page that lists a couple of dozens of products?
- User Authentication with multiple hidden pages: we don’t want to need to auth the user only on the client side. Let’s redirect the user on the server, i.e. BEFORE they get to the protected page.
- Localisation and internationalisation: I’ve done a bunch of projects that needed two or more languages. I’m doing one now that requires more than eight languages and more than ten geos/territories!
- Embedding Google Analytics and other tracking tools: because if it’s a client-facing site we _gots_ to know about our visitors, right?
- Design System in a Storybook: every company I joined (and I do mean EVERY) wanted their React components displayed neatly in a Storybook for all kinds of stakeholders to see/play with.
- Testing, incl. unit, integration and visual (i.e. snapshot testing).
- Server API function (or action) where secret keys can be used.
- Incremental Static Regeneration (ISR), for cases when we have hundreds or even thousands of pages.
- Deploy to a custom Docker-based environment somewhere to AWS Fargate, because not every company out there wants to use Vercel, Railway, and other “cool” and “hip” platforms.

So I did this silly “Frankenstein” project with those requirements. Let’s talk about the results!

## Findings

### Astro is EXPLICIT

The main conclusion that I got out of it is that Astro.js is like putting your project on a diet. On a JavaScript diet.

The thing about Astro is that it promotes the \*\*\*\*“reduced JavaScript” approach. That’s a whole other kind of mentality shift than what I initially was hoping for.

So it doesn’t render JavaScript unless you EXPLICITLY tell it to.

“Explicit” is the word I’d associate with Astro. While Next hides a lot of implementation details, Astro does nothing of such a thing and leaves implementation to us, developers.

If you want some interactivity with JavaScript on the front-end - you need to write it, e.g. `addListener`s in the `script` tags. Otherwise, Astro ships nothing to the browser.

Before I wasn’t really thinking explicitly much about how much JavaScript is and isn’t used in the browser. Most of the time, I just let frameworks figure this out for me, and only when we were starting to have issues with performance, etc. only then I start looking “under the hood” and optimising.

All in all, I’m happy with this approach. I kinda like it. Cognitively it feels easier to work this way.

This leads to the next point.

### React is just a sprinkle on top

Astro allows you to use ALL sorts of UI frameworks. Including React. BUT!

React is a UI library. And Astro is using it as such. RSC is not a thing there (yet?). So if you’re writing a `.tsx`/ `.jsx` file, you can be 100% sure it’s going to be rendered, live and work only in the browser.

Yes, you can certainly feed it server-fetched data, so the user doesn’t see the loader for too long. But it still remains a client only component.

For that reason, you can’t really use React ONLY or go too deep component-wise. They’re all end up on the client.

Do you want that? Is that a good thing? It depends on the project.

If you need to render things on the server first, say, for SEO reasons, and make it interactive, then I’d assume you’d need to have a conditional variable, e.g.

```jsx
const hasBeenChangedByUser = false

return {hasBeenChangedByUser ? <ReactComponent /> : <AstroComponent /> }
```

And both `<AstroComponent />` and `<ReactComponent />`, in this case, would look very similar. You could do a sharable code though, because [Astro still allows rendering `.tsx` component on the server](https://docs.astro.build/en/concepts/islands/#creating-an-island), it just strips them of all the JavaScript, making them redundant for React purpose but great for layout, etc.

In my experience, there weren’t many cases when we’d had to create something drastic like that, rather a healthy balance of server vs client. So this “React == UI only” approach doesn’t really bother me. I can render what I need on the server inside `.astro` component, and components that need to be overly interactive, they’re client in any framework anyway, because interactivity == JavaScript.

### No providers!

With Astro, we have two types of files: `.astro` and `.tsx`. Guess what `.astro` components can’t have that React just LOVES?

Providers!

Here’s another mentality shift for you. If you were thinking in a providers pattern, you need to snap out of it. You can no longer store your theme in a provider. No more [RadixUI](https://www.radix-ui.com/) or [MantineUI](https://mantine.dev/) for you.

So to have a theme and reusable CSS variables you’d need to go with how Tailwind works. Tailwind doesn’t use providers. It stores all the variables in it’s own config file.

Something like [NextUI](https://nextui.org/) would work, i.e. a “New Gen” UI framework. It’s Tailwind based AND it works in both `.astro` and `.tsx` files. Amazing!

To be frank, my last 4 projects were Tailwind-based. So I don’t see a problem with that. I can live with that choice.

Guess WHAT also generally uses providers?

State managers!

### The time has come for the new kind of state managers

We want to store state across Astro and React components. One state, two frameworks. How do you do that?

Framework agnostic state managers!

In its docs, Astro recommends [nanostores](https://github.com/nanostores/nanostores), but I’ve used [effector](https://effector.dev/) in the past. And LOVED IT. So I’ve used it for this project as well.

State managers like these are just JavaScript. They don’t care what frameworks they’re used with. They do have wrappers for them, e.g.

```jsx
import { useUnit } from "effector-react";
// ...
const { text, size } = useUnit({
  text: $text,
  size: $size,
});
```

but you can do without them.

Have you noticed that in v5 `@tanstack/query` moved on from using provider pattern for their QueryClient, e.g.

```jsx
import { QueryClient, QueryClientProvider } from "react-query";

const queryClient = new QueryClient();

function App() {
  return <QueryClientProvider client={queryClient}>...</QueryClientProvider>;
}
```

to a more function-based approach, e.g.

```jsx
import { QueryClient, useQuery } from '@tanstack/react-query'

const queryClient = new QueryClient()

function App() {
	const query = useQuery({ ... }, queryClient)
  //...
}
```

There’s a reason for that, I think. It’s more flexible, more universal, doesn’t lock you into a particular framework. You can fetch data on the server in `.astro` component, store it in your `queryClient` and then use it in a React component on the client with `useQuery`.

Switch React to Vue and you won’t need to touch the state in any way. Amazing refactoring experience, isn’t it? 🤯

Following the providers rant…

### i18n

Since there are no providers, we need to somehow communicate the language we’re working with to every component when setting up our localisation. So we do it this way 👇

```jsx
// In Astro component
---
const lang = getLangFromUrl(Astro.url);
---
return <Modal client:only="react" lang={lang} />

// Then in Modal.tsx
export const Modal = ({ lang }) => {
	const t = useTranslations(lang);

	return (
		<ModalHeader>
		   {t("modal.header")}
		</ModalHeader>
	)
}
```

So the language won’t be coming from a provider, but rather as a prop. And we’ll be getting it from our server `.astro` pages.

Which, again, to me isn’t a big deal. It’s just different. It works and it still looks nice. We can even define a “parent” interface that we’ll be using for all the prop types, e.g.

```jsx
interface PropsBase {
	lang: string
}

interface ComponentProp extends PropsBase {
	//...
}
```

### Astro is just like Next but more explicit

I keep coming back to that word.

In a lot of other aspects, Astro is not that different from Next developer experience and mentality-wise. I didn’t even feel pain when switching. I already knew where things would go, where things would come from, how to structure a project, etc.

Astro has its own place for `getServerSideProps` (or `use server`); it has an `api` folder you can create to store some serverless functions in; it runs in node and you can build a Dockerfile for it. and you can throw all your Google Analytics scripts to [PARTYTOWN](https://partytown.builder.io/google-tag-manager#forward-events), just like you’d do in Next.

And if you need to set up SSR authentication, you can do that, it’s pretty straightforward. I was able to set up one with [Supabase Auth](https://supabase.com/docs/guides/auth/server-side/email-based-auth-with-pkce-flow-for-ssr).

So it’s just like Next, but better.

Just a few ‘gotchas’ though…

### The “gotchas”

Astro has no runtime. This means no unit tests. This also means no Storybook for your Astro components (although, they’re [working on it](https://github.com/storybookjs/storybook/issues/18356)!)

Can you live with that?

I’d say yes. When it comes to testing, server-rendered components don’t have much interactivity going on anyway, no user clicks, no state changes, etc. So tests might be considered redundant or over-engineer.

And the layout render we can always test in e2e/integration/visual kinds of tests with Cypress or PlayWright.

As for Storybook, well, not everything needs to be in a Storybook. Surely we can find a nice balance.

### Astro needs more work

At the moment ISR comes down to cache [control/invalidation](https://logsnag.com/blog/implementing-on-demand-revalidation-in-astro) in SSR pages, not static page regeneration.

There is no per-page building, no ISR. If you rebuild static pages, you rebuild all of them.

The build is fast though, as [it was pointed out](https://astro.build/blog/experimental-static-build), Astro certainly can handle 10K+ pages.

Nonetheless, the Astro team is already [thinking and working on the build engine](https://github.com/withastro/roadmap/issues/698). So it will only become better.

Not even more confusing as it was in Next.js case, I hope… 😅

## To sum up

Astro is great. But it is slightly different from the way Next.js projects are done and not everyone would be happy to compromise. And that’s ok. We all have our own set of requirements, values, goals, the environment we work in, etc.

However, I think Astro is the future. And I don’t mean Astro framework. I think the Astro way of doing things: explicit, transparent, and minimalistic. This is the future where all the frameworks work as one, and where frameworks don’t overwrite standardised APIs and expose important bits to the developers allowing us to modify and hack it to each project’s own needs.

The future is simple, vanilla JavaScript because simplicity always wins. 🚀

For more talking and less reading, feel free to [watch my stream](https://www.linkedin.com/posts/nkakuev_is-astro-ready-for-proper-projects-doing-activity-7171165857065099267-UlVW?utm_source=share&utm_medium=member_desktop).