rsc-kit is a Vite plugin that gives you React Server Components. You write
pages, layouts and server actions the way you would in the Next.js App Router;
it renders them, streams the HTML, and turns links into navigations that replace
only the part of the page that changed.
It brings no build system of its own — it is a Vite plugin — and it does not write a server for you either. Nitro builds one around your route tree, so where an app runs is a preset: Bun, Node, a Cloudflare Worker, Vercel, Netlify, Deno.
What an app looks like
src/app/
layout.tsx the document — <html>, <head>, <body>
loading.tsx Suspense fallback for everything below
page.tsx GET /
posts/
[slug]/
page.tsx GET /posts/:slug
@modal/
default.tsx a parallel slot, empty until something fills itNothing registers those files. The plugin reads the directory at build time and generates the entry that knows about them.
import { findPost } from '../../../data'
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await findPost(slug)
if (!post) return <h1>No such post</h1>
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
)
}A server component is an ordinary async function. It imports its data module directly, and neither the module nor its secrets reach the browser.
What you get
Streaming, not waterfalls
The shell paints before the data resolves. Suspense boundaries fill in as they finish, in whatever order they finish.
Navigations that keep the page
A navigation sends the layout chain it already has, and the server answers with only the part that changed. Scroll, focus and half-typed forms survive.
Parallel routes and interception
@slot directories render alongside the page. (.)folder opens a route as
a modal over the page you were on, and as a real page on refresh.
Server actions
"use server" makes an async function callable from a client component.
The body never ships; the call becomes one POST.
Queries, without an endpoint
query() marks a read. It travels as a GET, so it can be cached and
prefetched — and reads that happen together leave as one request.
Frozen where it can be
Pages that ask for nothing dynamic are rendered at build time. Pages that do get their shell frozen and their data streamed.
An export target
The same app can build to a directory of files and be served by anything — including a CDN with no origin at all.
Where it runs
You do not write a server. Nitro builds one from your route tree, and where it runs is one line in your Vite config:
nitro({ preset: 'bun' })Swap the preset for node, cloudflare_module, vercel, netlify or deno
and the same app deploys there instead. The build produces a .output
directory; nothing else changes.