PostHole
Compose Login
You are browsing eu.zone1 in read-only mode. Log in to participate.
rss-bridge 2025-05-06T00:00:00+00:00

RSC for Astro Developers

Islands, but make it fractal.


RSC for Astro Developers

May 6, 2025

Pay what you like

Okay, so in Astro you have two things:

  • Astro Components: They have the .astro extension. They execute exclusively on the server or during the build. In other words, their code is never shipped to the client. So they can do things that client code cannot do—read from the filesystem, hit the internal services, even read from a database. But they can’t do interactive things aside from whatever exists natively in the HTML or your own <script>. Astro Components can render either other Astro Components or Client Islands.
  • Client Islands: Components written for React, Vue, and so on. This is your typical frontend stuff. That’s where it’s convenient to add the interactive bits. These Client Islands can then render other components for the same framework using that framework’s own mechanism. So, a React component can render another React component, as you would expect. But you can’t render an Astro Component from a Client Island. That wouldn’t make sense—by that point, Astro already ran.

Here’s a PostPreview.astro Astro Component rendering a LikeButton Island:

---
import { readFile } from 'fs/promises';
import { LikeButton } from './LikeButton';

const { slug } = Astro.props;
const title = await readFile(`./posts/${slug}/title.txt`, 'utf8');
---
<article>
<h1>{title}</h1>
<LikeButton client:load />
</article>
import { useState } from 'react';

export function LikeButton() {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'} Like
</button>

Notice how Astro Components and Client Islands essentially live in two different “worlds”, and the data only ever flows down. Astro Components are where all the preprocessing happens; they “hand off” the interactive bits to the Client Islands.

Now let’s look at React Server Components (RSC).

In RSC, the same two things are called Server Components and Client Components. Here is how you’d write the above Astro Component as a React Server Component:

import { readFile } from 'fs/promises';
import { LikeButton } from './LikeButton';

async function PostPreview({ slug }) {
const title = await readFile(`./posts/${slug}/title.txt`, 'utf8');
return (
<article>
<h1>{title}</h1>
<LikeButton />
</article>
'use client';

import { useState } from 'react';

export function LikeButton() {
const [liked, setLiked] = useState(false);

return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'} Like
</button>

The mental model behind these two are remarkably similar! If you know Astro, you already have 80% of the mental model for React Server Components. (Even if you think React Server Components are a terrible idea, Astro is worth learning.)

Let’s note a few syntactic differences you might have noticed above:

  • Unlike Astro Components, React Server Components are regular JavaScript functions. They are not “single-file”. The props are coming from the function argument rather than from Astro.props, and there is no separate “template”.
  • In Astro, the separation between Astro Components and Client Islands is achieved by writing the former as .astro files. Once you import a Client Island, you’re not in an .astro file anymore and thus you’re “leaving” the Astro world. In RSC, the same purpose is achieved by the 'use client' directive. The 'use client' directive marks where the Server world “ends”—it is a door between the worlds.
  • In Astro, there are directives like client:load that let you treat Islands either as static HTML or as hydratable on the client. React Server Components does not expose this distinction to the user code. From React’s perspective, if a component was written to be interactive, it would be a mistake to remove this interactivity. If a component truly does not require interactivity, just remove 'use client' from it, and then importing it from the Server world would already keep it Server-only.

The last point is interesting. In Astro, the different syntax (.astro files vs Client Islands) creates a sharp and obvious visual distinction between the two worlds. The same component can’t act as both an Astro Component and a Client Island depending on the context—they’re two distinct things with distinct syntaxes.

But in RSC, the “Astro” part is also “just React”. So if you have a component that doesn’t use any client-specific or server-specific features, it could play either role.

Consider a <Markdown /> component that does its own parsing. Since it doesn’t use any client features (no State) or any server features (no reading DB), it could be imported on either side. So if you import it from a Server world, it’ll act like an “Astro Component”, but if you import it from a Client world, it’ll act like a “Client Island”. This isn’t some new concept, it’s just how importing functions works!

In RSC, stuff imported from the Server world will run in the Server world; stuff that’s imported from the Client world will run in the Client world; and stuff that’s not supported in either world (e.g. DB on the Client or useState on the Server) will cause a build error so you’ll be forced to “cut a door” with 'use client'.

This is both a blessing and a curse.

It is a curse because it makes learning to wield RSC rather unintuitive. You keep worrying about “which world you’re in”. It takes practice to embrace that it doesn’t matter because you can always reason locally. You can use server features like DB in files that need them, use client features like State in files that need them, and rely on build-time assertions causing errors if something is wrong. Then you look at the module stack trace and decide where to “cut a new door” for your “islands”.

This is a curse, but it is also a blessing. By embracing React on both sides, the RSC model solves some Astro limitations that you might encounter along the way:

  • Sometimes, you might write a bunch of Astro Components and later realize that you’re gonna need to move that UI into Client Islands (tweaking the syntax along the way) or even duplicate it because some dynamic UI also wants to drive them. With RSC, you can extract the shared parts and import them from both sides. It is less important to think through “this part will mostly be dynamic” or “this part will mostly be static” for every piece of UI because you can always add or remove 'use client' and/or move it up or down the import chain with little friction. You do decide where to “cut the door”, but there’s no “converting” back and forth.
  • In Astro, you can nest Astro Components inside Client Islands, but if those include more Client Islands, they’ll still be seen as separate roots by your framework (e.g. React). This is why nesting interactive behavior doesn’t compose as naturally as in client apps, e.g. React or Vue context can’t be passed between Astro islands. In RSC, this is not a problem—the entire UI is a single React tree under the hood. You can have a Client context provider above some Server subtree, and then a bunch of Client components reading that context anywhere below. RSC is fractal islands.

[...]


Original source

Reply