A couple weeks ago Michael Jackson, one of the authors of React Router, dropped this bomb on Twitter (emphasis mine):

React Server Components are nice in theory, but 5 years in, it just isn’t working out. It’s been fun, React. You taught me a lot. Have fun with your react-server-dom-esm-vite-client/server bundles. I’m done. ✌️

To which Evan You, author of Vue, followed up with (emphasis also mine):

I don’t think RSC itself being a React feature is a problem - it unlocks some interesting patterns. The issue is the tradeoffs involved in making it work. It leaks into, or even demands control over layers that are previously not in scope for client-side frameworks. This creates heavy complexity (and associated mental overhead) in order to get the main benefits it promises. React team made a bet that they can work with Next team to polish the DX to the extent that the benefit would essentially be free - so that RSC can be a silver bullet for all kinds of apps, and that it would become the idiomatic way to use React. IMO, that bet has failed.

Further adding:

I held this prediction very early on, but held off expressing it explicitly because I always respect how React team is willing to rethink best practices - some of those bets paid off handsomely - and I understand sometimes you gotta give new ideas the chance to iterate. But deep down I never liked the direction, which is why I decided early on that we will not do something similar in Vue. Some of the benefits of RSC in static generation cases can be achieved without such a drastic paradigm shift. [...]

You see, Evan is a class act. I think Vue benefited greatly from React's team inventiveness with hooks. But as I myself went through the pain of attempting to figure RSC out for @fastify/react, an urge to write a piece in the molds of the best of Steve Yegge started to build inside of me.

React has become unbelievably unavoidable. Unless you are a developer working on your own product, chances are, you're going to be forced to use React at least once in your career. Maybe it'll be the COBOL of the web development world.

Fifty years from now there could still be some mega corporation depending heavily on React, trying to grab an ancient patch release of react-server-dom-webpack lingering somewhere.

The first time I heard about RSC features being rolled out in Next.js, with no documentation anywhere to be found for other framework authors, I decided to ignore it. Still, my Twitter and YouTube feeds were all about RSC — with people close to me waxing lyrically about it and rushing to deploy the latest and shiniest Next.js builds to production. And suddenly, implementing RSC started to become a priority in the back of my mind. All the meanwhile, I'm trying to finish a book that has changed scope so many times by now that I didn't want to further delay it for my hopes of supporting RSC in @fastify/react.

Learning RSC

Since there was no official documentation for framework authors for a long time — well, to be fair, there still isn't — the way to learn RSC was to dive deep into Next.js and reverse engineer things. People have written extensively about it, and made me feel hopeful at times. Daniel Nagy's piece, focused on Vite integration, was the one that finally triggered me to at least give it a shot.

Also recommend Josh W. Comeau's amazing write-up, as well as Mayank's, Timothée Pillard's and Kent C. Dodd's comprehensive workshop. The fact that people like Kent exist makes React suck less.

The first thing one notices when trying to work with RSC is the react-server-dom-webpack package. Let's take a moment now to reflect on the absurdity of 527,000+ people downloading at a package whose README clearly states:

As of today, I still can't find documentation for this package. I looked through React 19's documentation, no mention of it. Alas, with some effort, Webpack shenanigans can be worked around in Vite. But it surely would be nice to have a bundler-agnostic package or even better, one dedicated to Vite.

A Dedicated Package for Vite

Jacob Ebey started work on implementing react-server-dom-vite, which faced some criticism from the React team.

Sebastian Markbåge wrote:

It's still to unopinionated. We want you to have an opinion about the right way of doing things for Vite. E.g. there's a bunch of little issues here (like sync module invocation and CSS loading) that we can discuss in a follow up or I can even fix myself. There's many things we don't like about the current interface that we want to improve. If there's 8 different ways of doing things for Vite we can't go around having a discussion about it in each one. [...]

To which Jacob responded:

As @hi-ogawa mentioned, the Vite ecosystem is pretty "piece things together". CSS loading for example is implemented individually by everyone and does not expect to be standardized. [...]

I like Jacob's implementation very much and for one, think the opinionated part of it should come in the form strong recommendations in the documentation, as well as working examples. It's hard to step away from the norm if things just work. There's at least one plugin out there already using Jacob's package.

The Vite team has formed an internal RSC workgroup — which I'm part of — and hopefully things will start moving again.

Initial RSC Support in @fastify/react

Thanks to the work of Hiroshi Ogawa and his venture into uncharted territory, we're now finally close to having full RSC support in @fastify/react, but it's still riddled with bugs and far from ideal — mainly because it was built still trying to integrate react-server-dom-webpack in Vite. I'm starting a refactor to try and use the new experimental react-server-dom-vite mentioned above.

Introducing @fastify/react v1

As I said earlier, I'm trying to finish a book that's well past overdue and the main reason it didn't go out yet is that I, being the stubborn idiot that I am, wanted to have RSC support in @fastify/react and cover it in the book — and spent way too much time on it. It's been too long, and I'm still ways to get it to a satisfactory point, so I decided to release @fastify/react with support for React 19 but not RSC. Think of it as a mini Next.js, with some batteries included.

Here's a mini tutorial (requires Node v22+):

  1. Install these packages (listed one by one for clarity):
% pnpm add fastify
% pnpm add @fastify/vite
% pnpm add @fastify/react
% pnpm add react
% pnpm add react-dom
% pnpm add react-router
% pnpm add unihead
% pnpm add valtio
% pnpm add history
% pnpm add -D @vitejs/plugin-react
% pnpm add -D vite

Yep, that's it. That's a lot, you say?

% cd fastify-react-app
% du -sh node_modules
103M  node_modules

% cd next-app
% du -sh node_modules
285M  node_modules

Next.js does so much by bundling a lot of stuff into it. Including its own server, and using it with Fastify relies on running everything through the Next.js middleware. For people trying to get the maximum performance out of Node.js, running Next.js is a bit of a nightmare — you're completely dependent on it and there's very little room for customization. You don't own the server code.

In @fastify/vite, which @fastify/react is based on, everything's customizable.

Don't like how a route handler is setup?

Just provide your own createRoute() hook.

Need a few extra things loaded on the server?

Provide your own prepareClient() hook.

  1. Create a vite.config.js file:
import { join } from 'node:path'
import viteReact from '@vitejs/plugin-react'
import viteFastifyReact from '@fastify/react/plugin'

export default {
  root: join(import.meta.dirname, 'client'),
  plugins: [
    viteReact(),
    viteFastifyReact(),
  ],
}

@fastify/react requires a Vite configuration file. Although it's technically possible to omit this file altogether, that would be the direction of turning it into a full blown framework. So requiring a Vite configuration file is by design. It opens up your app to Vite's ecosystem, and provides a clean way to inject some things into the application build process.

Internally, @fastify/react bundles an application shell with the absolute minimum code necessary to have a few key features, such as: pages/ folder based router setup, seamless SSR/CSR rendering and router navigation, plus the ability to export certain things from route modules to manage <head> tags and data fetching. These files right here, leveraging the magic of virtual modules.

But... don't like what you see in create.jsx? Just copy it over to your project's Vite root folder and modify it as you wish. @fastify/react will identify it and use it instead. Everything can be overriden.

  1. Create a client/index.html file:
<div id="root"><!-- element --></div>
<script type="module" src="/$app/mount.js"></script>

This file is required by design. For obvious reasons. No reason to omit it and serve some default template. Your application needs an HTML shell. This is it.

  1. Create a client/pages/index.jsx file:
export default function Home() {
  return <h1>Hello world</h1>
}

Automatically gets registered as GET /. You can add:

export const path = '/something-else'

To override the default /pages folder based registration.

  1. Create a server.js file:
import Fastify from 'fastify'
import FastifyVite from '@fastify/vite'

const server = Fastify()
await server.register(FastifyVite, {
  root: import.meta.url,
  renderer: '@fastify/react',
})

await server.vite.ready()
await server.listen({ port: 4000 })

If you wanted to override the createRoute() hook:

import Fastify from 'fastify'
import FastifyVite from '@fastify/vite'
import FastifyReact from '@fastify/react'

const server = Fastify()
await server.register(FastifyVite, {
  root: import.meta.url,
  renderer: {
    ...FastifyReact,
    createRoute(entries) {
      // ...
    }
  },
})

In a nutshell, @fastify/react reads your pages/ folder and loads every JavaScript module it can find as a route module. And for every route module, @fastify/vite will run createRoute() on it registering an individual server route for each of the client routes that you have. The createRoute() hook provided by @fastify/react takes care of everything — SSR, streaming, hydration, very much like Next.js.

  1. Run it in development:
node server.js --dev
  1. Build it:

I placed a screenshot here to highlight two things: a) in this setup, only your client code needs a build phase — and b) a single vite build command will bundle your client and server builds. The server build is loaded by @fastify/react for SSR.

  1. Run it:
node server.js
  1. Test it:
curl http://localhost:4000
<script type="module" crossorigin src="/assets/index-EtiN2kYP.js"></script>
<div id="root"><!--$--><!--$--><h1>Hello world</h1><!--/$--><!--/$--></div>

@fastify/react v1 is covered in Happy Little Monoliths. I'm a bit sad RSC didn't make it to the release nor the book. But now I have everything I need to finally release it. I've been bumping the release date every day for the past week or so while I made up my mind on it, and pulled a few more all nighters trying to get RSC to fully work in @fastify/react, but this is it. I've bumped the book's release date one last time while I update all the code examples, but finally, this is it.

RSC support will still come in a future release, hopefully sooner than later. But still, I'm left with a bitter taste thinking about all the time I devoted to something that, in the end, might not be necessary after all, or as Evan puts it:

RSC (and its Next-based implementation) definitely works for some users. But the reality is many more are struggling with it and finding it unnecessarily complex for the marginal benefits (if not negative).

I think @fastify/react v1 is an amazing release, not because it's packed with features — well, it is, to some extent — but rather because it's architected in a completely open and straightforward way, with a micro application shell that's basically asking for contributions.

But depending on the nature of the contributions, I still intend to keep the default application shell as small as possible.

Maybe a @fastify/react-plus of sorts could become a thing, but as a separate renderer package. My vision is that both @fastify/vue and @fastify/react remain as simple and minimal as they can possibly be.