FFUNSTACK Static
DocsAPILearn

Getting Started

IntroductionMigrating from Vite SPA

Learn

React Server ComponentsHow It WorksOptimizing RSC PayloadsUsing lazy() in ServerPrefetching with ActivityFile-System Routing

Advanced

Multiple Entrypoints (SSG)Server-Side Rendering

API Reference

funstackStatic()defer()EntryDefinition

Help

FAQ

React Server Components

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.

What Are Server Components?

Server Components are React components that:

  • Run on the server/build time - Not in the browser
  • Can be async - Use async/await directly in components
  • Have zero client bundle impact - Their code never ships to the browser
  • Can access server-side resources - Files, databases, environment variables
// 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 and RSC

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:

  • Reduced bundle size
  • Build-time data fetching
  • Async components
// 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.

Composing Server and Client Components

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>
  );
}

Benefits for SPAs

1. Reduced Bundle Size

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.

2. Reduced Warm-Up Time

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.

3. Build-Time Data Fetching

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} />;
}

4. Full SPA Interactivity

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}`);
        }
      }}
    />
  );
}

5. Type-Safe Data Flow

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} />;
}

Considerations

When using FUNSTACK Static, keep in mind:

  1. Build-time data - Server Component data is fetched during build. For runtime data, use Client Components with standard fetch patterns.
  2. No server context - No access to cookies, headers, or request data in Server Components.
  3. Only one entrypoint - FUNSTACK Static apps are single-page applications, without any routing built in. All routing and navigation must be handled client-side.

This makes FUNSTACK Static ideal for developers looking to leverage RSC benefits while deploying simple static SPAs without server infrastructure.

See Also

  • Getting Started - Set up your first project
  • defer() - Stream content progressively
  • funstackStatic() - Plugin configuration