Skip to main content

React Server Components

Answer​

React Server Components (RSC) are components that render on the server and send HTML to the client, reducing JavaScript bundle size and enabling direct backend access.

Client vs Server Components​

Basic Usage​

// Server Component (default in Next.js 13+ app router)
// Can directly access databases, file systems, etc.
async function ProductList() {
const products = await db.query("SELECT * FROM products");

return (
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}

// Client Component (needs 'use client' directive)
("use client");

import { useState } from "react";

function Counter() {
const [count, setCount] = useState(0);

return <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>;
}

When to Use Which​

Use CaseComponent Type
Database queriesServer
API tokens/secretsServer
Static contentServer
Large dependenciesServer
onClick, onChangeClient
useState, useEffectClient
Browser APIsClient
Custom hooks with stateClient

Composing Server and Client​

// Server Component
import InteractiveButton from "./InteractiveButton";

async function BlogPost({ id }) {
const post = await fetchPost(id); // Server-side fetch

return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>

{/* Client component nested in server component */}
<InteractiveButton postId={id} />
</article>
);
}

// Client Component
("use client");

function InteractiveButton({ postId }) {
const [liked, setLiked] = useState(false);

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

Passing Server Data to Client​

// ✅ Pass serializable data as props
async function UserProfile() {
const user = await fetchUser(); // Server

return (
<div>
<h1>{user.name}</h1>
<ClientSettings initialSettings={user.settings} />
</div>
);
}

// ❌ Cannot pass functions, classes, Dates directly
// Must serialize first

Rendering Flow​

Benefits​

// 1. Zero client-side JavaScript for server components
// Large dependencies don't increase bundle size

// Server Component - marked won't be in client bundle
import { parse } from "marked"; // 50kb library

async function MarkdownPost({ content }) {
const html = parse(content); // Runs on server
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

// 2. Direct data access - no API endpoints needed
async function Dashboard() {
const [users, orders, revenue] = await Promise.all([
db.users.count(),
db.orders.findRecent(),
db.orders.calculateRevenue(),
]);

return (
<div>
<Stat label="Users" value={users} />
<Stat label="Orders" value={orders.length} />
<Stat label="Revenue" value={revenue} />
</div>
);
}

// 3. Secrets stay on server
async function ProtectedData() {
const data = await fetch("https://api.example.com", {
headers: {
Authorization: `Bearer ${process.env.API_SECRET}`,
},
});
// API_SECRET never sent to client
}

Patterns​

// Pattern: Fetch then render (parallel fetches)
async function Page() {
const [posts, comments] = await Promise.all([fetchPosts(), fetchComments()]);

return (
<>
<PostList posts={posts} />
<CommentSection comments={comments} />
</>
);
}

// Pattern: Streaming with Suspense
async function Page() {
return (
<>
<Header /> {/* Immediate */}
<Suspense fallback={<PostsSkeleton />}>
<PostList /> {/* Streamed when ready */}
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments /> {/* Streamed when ready */}
</Suspense>
</>
);
}

Limitations​

// ❌ No hooks that use state
// ❌ No browser-only APIs (window, localStorage)
// ❌ No event handlers
// ❌ Cannot be dynamically imported on client

// Server component cannot:
function ServerComponent() {
const [state, setState] = useState(); // ❌ Error

useEffect(() => {}); // ❌ Error

return <button onClick={() => {}}> // ❌ Error
}

Key Points​

  • Server Components run on server, zero bundle impact
  • Use 'use client' directive for interactive components
  • Server components can't use hooks or event handlers
  • Pass only serializable props to client components
  • Direct database/API access in server components
  • Combine with Suspense for streaming
  • Default in Next.js 13+ app router