React Server Components (RSCs) let you render React components on the server, sending down only the necessary JavaScript to the client.
Let’s see it in action. Imagine a simple ProductDetails component that fetches data from an API.
Server Component (app/product/[id]/page.tsx):
import Image from 'next/image';
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`);
if (!res.ok) {
throw new Error('Failed to fetch product');
}
return res.json();
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
return (
<div>
<h1>{product.name}</h1>
<Image src={product.imageUrl} alt={product.name} width={300} height={300} />
<p>{product.description}</p>
{/* Client component interaction example */}
<AddToCartButton productId={product.id} />
</div>
);
}
Client Component (components/AddToCartButton.tsx):
'use client'; // Mark this as a client component
import { useState } from 'react';
export default function AddToCartButton({ productId }: { productId: string }) {
const [isAdding, setIsAdding] = useState(false);
const handleClick = async () => {
setIsAdding(true);
// Imagine calling an API to add to cart
await new Promise(resolve => setTimeout(resolve, 1000));
setIsAdding(false);
alert('Added to cart!');
};
return (
<button onClick={handleClick} disabled={isAdding}>
{isAdding ? 'Adding...' : 'Add to Cart'}
</button>
);
}
When a user navigates to /product/123, Next.js (which implements RSCs) executes app/product/[id]/page.tsx on the server. It fetches product data, renders the <h1>, <Image>, and <p> tags. Crucially, it doesn’t run the AddToCartButton component’s JavaScript on the server. Instead, it sends down a special "bundle" for AddToCartButton that instructs the client-side React to hydrate that component. The Image component from next/image is also optimized, rendering as a standard <img> tag on the server with appropriate attributes.
The core problem RSCs solve is over-fetching and over-bundling. Traditionally, all your React code runs on the client. This means even components that only display static data need to be downloaded, parsed, and executed by the browser. With RSCs, you can offshore rendering and data fetching to the server. The server renders the component to HTML and sends down only the minimal JavaScript required for interactive elements (client components). This leads to significantly faster initial load times and reduced client-side processing.
Internally, RSCs are serialized into a special format, often referred to as "React Server Component artifacts" or "RSC payload." This payload contains directives for the client-side React runtime. For example, it might contain instructions like: "render this div with these children, and for this AddToCartButton component, load its client-side JavaScript and hydrate it with these props." The Image component, when used server-side, renders directly to an <img> tag. The next/image component’s client-side part is only responsible for things like lazy loading and placeholder management if you were to use those features, but the core image rendering happens on the server.
The key levers you control are which components are marked as 'use client' and where you place them in your component tree. Any component not marked with 'use client' can be a Server Component. Server Components can import and render Client Components, but Client Components cannot import and render Server Components directly. This unidirectional dependency is fundamental to the architecture. You can also pass props from Server Components to Client Components, including event handlers, though these handlers will execute on the client.
The trickiest part is understanding how data flows and how state is managed. Server Components are effectively static for a given request; their state is determined by the data fetched during that server render. Client Components, marked with 'use client', are where you’ll use useState, useReducer, and other hooks to manage interactive state. When a prop changes from a parent Server Component to a child Client Component, React will re-render the Server Component, fetch new data if necessary, and then send an updated RSC payload to the client. The client-side React then merges these updates into the existing client component’s state and UI.
The next hurdle is understanding how to optimize data fetching strategies within RSCs, particularly when dealing with complex, nested data requirements and avoiding redundant fetches across multiple Server Components.