React Server Components (RSC) are a new paradigm for building React applications where components can run on the server (or at build time) rather than in the browser.
Server Components are React components that:
async/await directly in components// This component runs at build time, not in the browser
async function UserList() {
// Direct file system access
const data = await fs.readFile("./data/users.json", "utf-8");
const users = JSON.parse(data);
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
FUNSTACK Static is a React framework without a runtime server that still leverages React Server Components to improve performance of traditional SPAs.
All server components are rendered at build time, producing RSC Payloads that are fetched by the client (browser) at runtime.
While dynamic server-side logic isn't possible with FUNSTACK Static, you can still benefit from RSC features like:
// Build-time data fetching with an async Server Component
async function BlogPost({ slug }: { slug: string }) {
// Runs during build, not at runtime
const content = await fetchMarkdownFile(`./posts/${slug}.md`);
return (
<article>
<Markdown content={content} />
</article>
);
}
Note: in FUNSTACK Static, async data is fetched at build time. For truly dynamic data, you'll need client-side JavaScript and fetch data as you would do in pre-RSC SPAs.
While Server Components can't use hooks or browser APIs, they can render Client Components that do:
// Server Component (runs at build time)
async function ProductPage({ id }: { id: string }) {
const product = await fetchProduct(id);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Client Component for interactivity */}
<AddToCartButton productId={id} />
</div>
);
}
// Client Component (runs in browser)
"use client";
import { useState } from "react";
export function AddToCartButton({ productId }: { productId: string }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => setAdded(true)}>
{added ? "Added!" : "Add to Cart"}
</button>
);
}
Unlike traditional SPAs where all code ships to the client, Server Components never reach the browser. Only static HTML (and glue code for client components) is sent.
The browser does't need to execute Server Component code, leading to less JavaScript to parse and run on initial load. Only ship the code needed for interactivity.
Fetch data once at build time, embedded directly into your pre-rendered HTML:
async function BlogIndex() {
// Fetched once during build, not on every page view
const posts = await fetchAllPosts();
return <PostList posts={posts} />;
}
On the browser, your app behaves like any SPA - client-side navigation, state management, and all the interactivity you need:
// Client Component for interactive features
"use client";
import { useState } from "react";
import { useNavigate } from "@funstack/router";
export function SearchBox() {
const [query, setQuery] = useState("");
const navigate = useNavigate();
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
navigate(`/search?q=${query}`);
}
}}
/>
);
}
Data flows from Server Components to Client Components with full TypeScript support:
interface Post {
id: string;
title: string;
content: string;
}
async function BlogPost({ slug }: { slug: string }): Promise<JSX.Element> {
const post: Post = await fetchPost(slug);
return <Article post={post} />;
}
When using FUNSTACK Static, keep in mind:
This makes FUNSTACK Static ideal for developers looking to leverage RSC benefits while deploying simple static SPAs without server infrastructure.