React Server Components Interview Questions (Free Preview)
Free sample of 15 from 45 questions available
React Server Components Fundamentals
What is the difference between Server Components and Client Components?
The 30-Second Answer: Server Components render on the server, can access server resources directly, and send zero JavaScript to the client. Client Components render on both server (for SSR) and client, can use hooks and interactivity, but add to the JavaScript bundle.
The 2-Minute Answer (If They Want More): The fundamental difference lies in where and how these components execute. Server Components run exclusively on the server during the request/response cycle. They can access databases, read files, call internal APIs, and use server-only Node.js APIs. The output is serialized and sent to the client, but the component code itself never reaches the browser. This means no JavaScript bundle cost for Server Components.
Client Components, marked with the 'use client' directive, are the traditional React components we're familiar with. They are first rendered on the server for Server-Side Rendering (SSR) to provide fast initial HTML, then the JavaScript is sent to the client where the component hydrates and becomes interactive. Client Components can use hooks like useState, useEffect, and can respond to browser events. They're necessary for any interactive UI elements.
There are important compositional rules: Server Components can import and render Client Components, passing server data as props. However, Client Components cannot import Server Components directly because the client can't execute server-only code. If you need to nest a Server Component inside a Client Component, you must pass it as children or props (the "composition pattern").
Server Components are the default in frameworks like Next.js 13+ App Router. Any component without 'use client' is a Server Component. This default makes applications faster by default, requiring developers to explicitly opt into client-side JavaScript only when needed for interactivity.
Code Example:
// app/page.jsx - Server Component (default)
import { headers } from 'next/headers';
import UserProfile from './UserProfile'; // Client Component
import DatabaseStats from './DatabaseStats'; // Server Component
export default async function Dashboard() {
// Server-only: access headers, database, filesystem
const headersList = headers();
const userAgent = headersList.get('user-agent');
// Async data fetching directly
const data = await fetch('https://api.example.com/stats', {
// Server-side caching
next: { revalidate: 3600 }
}).then(res => res.json());
return (
<div>
{/* Server Component rendering another Server Component */}
<DatabaseStats />
{/* Server Component passing data to Client Component */}
<UserProfile
stats={data}
userAgent={userAgent}
/>
</div>
);
}
// UserProfile.jsx - Client Component
'use client';
import { useState, useEffect } from 'react';
export default function UserProfile({ stats, userAgent }) {
const [isExpanded, setIsExpanded] = useState(false);
// Can use effects and browser APIs
useEffect(() => {
console.log('User agent:', userAgent);
}, [userAgent]);
// Can handle events
const handleToggle = () => {
setIsExpanded(!isExpanded);
};
return (
<div onClick={handleToggle}>
<h2>Profile</h2>
{isExpanded && <pre>{JSON.stringify(stats, null, 2)}</pre>}
</div>
);
}
// WRONG: Client Component importing Server Component
// 'use client';
// import ServerComponent from './ServerComponent'; // ❌ Error!
// CORRECT: Composition pattern
// 'use client';
// export default function ClientWrapper({ children }) {
// return <div className="wrapper">{children}</div>;
// }
// Usage: <ClientWrapper><ServerComponent /></ClientWrapper>
Mermaid Diagram (if helpful):
flowchart LR
subgraph Server
SC[Server Component]
SC --> DB[(Database)]
SC --> FS[File System]
SC --> API[Internal APIs]
SC --> Serialize[Serialize UI]
end
subgraph Network
Serialize --> Stream[Streamed Response]
end
subgraph Client
Stream --> HTML[Initial HTML]
Stream --> Hydrate[Hydrate Client Components]
CC[Client Component]
CC --> Hooks[React Hooks]
CC --> Events[Browser Events]
CC --> State[Local State]
end
style SC fill:#90EE90
style CC fill:#87CEEB
References:
- Next.js Server and Client Components
- React.dev: Client Components
- Patterns for Server and Client Composition
What is the "use client" directive and when do you need it?
The 30-Second Answer: The 'use client' directive marks a file's boundary between server and client code. You need it when a component uses React hooks (useState, useEffect), handles browser events, accesses browser APIs, or depends on client-only libraries.
The 2-Minute Answer (If They Want More): The 'use client' directive is a build-time instruction that defines the transition point from server-executed code to client-executed code. When the bundler encounters this directive, it knows to include this module and all its dependencies in the client JavaScript bundle and to set up the necessary hydration boundaries.
You need 'use client' in several specific scenarios. The most common is when using React hooks for state management (useState, useReducer) or side effects (useEffect, useLayoutEffect). These hooks only work in Client Components because they rely on React's client-side runtime. Similarly, any component that handles user interactions like onClick, onChange, or onSubmit needs to be a Client Component since these events only occur in the browser.
Browser APIs are another clear indicator. If your component uses window, document, localStorage, geolocation, or any Web API, it must be a Client Component. Third-party libraries that depend on browser features (like animation libraries, charting libraries with interactivity, or form validation libraries) also require 'use client'.
Context providers that use React Context API typically need 'use client' because they often manage state. However, you can create patterns where the provider itself is a Client Component but the consuming components can be Server Components if they don't need interactivity.
The key principle is to use 'use client' as sparingly as possible. Push it down the component tree to the smallest components that actually need client-side features. This maximizes the amount of your application that benefits from Server Component optimizations while still enabling rich interactivity where needed.
Code Example:
// Scenario 1: State Management - Needs 'use client'
'use client';
import { useState } from 'react';
export default function SearchBox() {
const [query, setQuery] = useState('');
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
);
}
// Scenario 2: Event Handlers - Needs 'use client'
'use client';
export default function LikeButton() {
const handleClick = () => {
alert('Liked!');
};
return <button onClick={handleClick}>Like</button>;
}
// Scenario 3: Browser APIs - Needs 'use client'
'use client';
import { useEffect, useState } from 'react';
export default function WindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
// window only exists in browser
const updateSize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
updateSize();
window.addEventListener('resize', updateSize);
return () => window.removeEventListener('resize', updateSize);
}, []);
return <div>Window: {size.width} x {size.height}</div>;
}
// Scenario 4: Client-Only Library - Needs 'use client'
'use client';
import { motion } from 'framer-motion'; // Animation library
export default function AnimatedCard({ children }) {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
{children}
</motion.div>
);
}
// Scenario 5: Context Provider - Usually needs 'use client'
'use client';
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
// Scenario 6: DON'T need 'use client' - Server Component
// StaticContent.jsx (no directive needed)
export default async function StaticContent() {
const data = await fetch('https://api.example.com/data').then(r => r.json());
// No hooks, no events, no browser APIs
return (
<div>
<h1>{data.title}</h1>
<p>{data.description}</p>
</div>
);
}
// Pattern: Composition to minimize client boundaries
// Page.jsx - Server Component
import InteractiveSection from './InteractiveSection';
export default async function Page() {
const data = await fetchData();
return (
<div>
{/* Most of page is Server Component */}
<header>{data.header}</header>
<main>{data.content}</main>
{/* Only interactive part is Client Component */}
<InteractiveSection initialData={data.interactive} />
<footer>{data.footer}</footer>
</div>
);
}
// InteractiveSection.jsx - Client Component
'use client';
import { useState } from 'react';
export default function InteractiveSection({ initialData }) {
const [expanded, setExpanded] = useState(false);
return (
<section>
<button onClick={() => setExpanded(!expanded)}>
Toggle
</button>
{expanded && <div>{initialData}</div>}
</section>
);
}
Mermaid Diagram (if helpful):
flowchart TD
A{Does component need...?} --> B[React Hooks?]
A --> C[Event Handlers?]
A --> D[Browser APIs?]
A --> E[Client-only Libraries?]
A --> F[None of above?]
B --> G[âś… use client]
C --> G
D --> G
E --> G
F --> H[❌ Keep as Server Component]
G --> I[Component added to client bundle]
G --> J[Can use interactivity]
G --> K[Increases bundle size]
H --> L[Zero client JavaScript]
H --> M[Direct server access]
H --> N[Better performance]
style G fill:#87CEEB
style H fill:#90EE90
References:
↑ Back to topWhat is the "use server" directive and what does it enable?
The 30-Second Answer: The 'use server' directive marks functions as Server Actions that can be called from Client Components, enabling secure server-side mutations and data operations directly from the client without creating API routes.
The 2-Minute Answer (If They Want More): The 'use server' directive creates Server Actions - special functions that execute on the server but can be invoked from Client Components. This is the opposite flow from 'use client': instead of bringing server code to the client, it allows the client to call server functions remotely. When you mark a function with 'use server', the framework generates an endpoint for it and replaces the function call in your client code with a network request to that endpoint.
Server Actions are particularly powerful for form submissions, data mutations, and operations that require server-side security or resource access. They can access databases, modify files, send emails, or perform any server-only operation. Because they execute on the server, they can safely use environment variables, credentials, and perform security checks without exposing sensitive logic to the client.
The directive can be used in two ways: at the top of a file to mark all exported functions as Server Actions, or inline within a single function to mark just that function. Server Actions automatically work with React's experimental form actions feature, enabling progressive enhancement where forms work without JavaScript and enhance with JavaScript when available.
Server Actions receive serialized arguments and can return serialized values. They integrate seamlessly with React's useTransition and useFormStatus hooks to provide pending states and optimistic updates. Error handling happens naturally - errors thrown in Server Actions are caught and can be handled in Client Components.
This pattern eliminates the need for many traditional API routes, reducing boilerplate while maintaining security. The code is more maintainable because the mutation logic lives next to the component that uses it, rather than in a separate API directory. Server Actions are a key part of making the Server Component architecture feel cohesive and developer-friendly.
Code Example:
// Pattern 1: File-level 'use server' - all exports are Server Actions
// actions.js
'use server';
import { db } from '@/lib/database';
import { revalidatePath } from 'next/cache';
export async function createPost(formData) {
// Runs on server - can access database
const title = formData.get('title');
const content = formData.get('content');
// Server-side validation
if (!title || title.length < 3) {
return { error: 'Title must be at least 3 characters' };
}
// Database mutation
const post = await db.post.create({
data: { title, content }
});
// Revalidate cache
revalidatePath('/posts');
return { success: true, post };
}
export async function deletePost(postId) {
await db.post.delete({ where: { id: postId } });
revalidatePath('/posts');
}
// Pattern 2: Inline 'use server' - single function
// PostForm.jsx
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Saving...' : 'Save Post'}
</button>
);
}
export default function PostForm() {
async function handleSubmit(formData) {
'use server';
// This function runs on server
const title = formData.get('title');
await db.post.create({ data: { title } });
}
return (
<form action={handleSubmit}>
<input name="title" required />
<SubmitButton />
</form>
);
}
// Pattern 3: Using Server Actions from Client Components
// CreatePostForm.jsx
'use client';
import { createPost } from './actions';
import { useState, useTransition } from 'react';
export default function CreatePostForm() {
const [isPending, startTransition] = useTransition();
const [result, setResult] = useState(null);
async function handleSubmit(formData) {
startTransition(async () => {
const response = await createPost(formData);
setResult(response);
});
}
return (
<form action={handleSubmit}>
<input name="title" placeholder="Title" required />
<textarea name="content" placeholder="Content" />
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create Post'}
</button>
{result?.error && <p className="error">{result.error}</p>}
{result?.success && <p className="success">Post created!</p>}
</form>
);
}
// Pattern 4: Progressive Enhancement
// SignupForm.jsx
'use client';
import { signup } from './actions';
export default function SignupForm() {
return (
// Works without JavaScript, enhanced with JavaScript
<form action={signup}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Sign Up</button>
</form>
);
}
// actions.js
'use server';
import { redirect } from 'next/navigation';
export async function signup(formData) {
const email = formData.get('email');
const password = formData.get('password');
// Hash password, create user
const user = await createUser(email, password);
// Set session
await setSession(user.id);
// Redirect after successful signup
redirect('/dashboard');
}
// Pattern 5: Server Actions with arguments (not FormData)
// LikeButton.jsx
'use client';
import { toggleLike } from './actions';
import { useState, useTransition } from 'react';
export default function LikeButton({ postId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const [isPending, startTransition] = useTransition();
function handleClick() {
startTransition(async () => {
// Call Server Action with regular arguments
const newCount = await toggleLike(postId);
setLikes(newCount);
});
}
return (
<button onClick={handleClick} disabled={isPending}>
❤️ {likes} {isPending && '...'}
</button>
);
}
// actions.js
'use server';
export async function toggleLike(postId) {
const post = await db.post.findUnique({ where: { id: postId } });
const newLikes = post.likes + 1;
await db.post.update({
where: { id: postId },
data: { likes: newLikes }
});
return newLikes;
}
Mermaid Diagram (if helpful):
sequenceDiagram
participant Client
participant Framework
participant Server
participant Database
Client->>Client: User submits form
Client->>Framework: Call Server Action
Framework->>Server: POST /action-endpoint
Server->>Server: Execute 'use server' function
Server->>Database: Mutation query
Database-->>Server: Success
Server->>Server: Revalidate cache
Server-->>Framework: Return result
Framework-->>Client: Update UI
Client->>Client: Show success state
Note over Client,Database: All security on server<br/>No API routes needed
References:
↑ Back to topData Fetching
What is the difference between fetching data in Server Components vs Client Components?
The 30-Second Answer: Server Components fetch data on the server using async/await directly in the component, with access to backend resources and no client bundle impact. Client Components fetch on the client using hooks like useEffect or libraries like SWR, running after the component mounts in the browser.
The 2-Minute Answer (If They Want More): The fundamental difference lies in where and when the data fetching occurs. Server Components fetch data during the server render, before any HTML is sent to the client. This means you can access databases, file systems, and private APIs directly without exposing credentials or adding code to the client bundle. The data is fetched once per request (or cached), and only the rendered result is sent to the browser.
Client Components, marked with 'use client', fetch data in the browser after the component mounts. This requires using hooks like useEffect, useState, or data fetching libraries. The fetching code is included in the JavaScript bundle sent to the client, increasing bundle size. Client-side fetching creates a waterfall effect: HTML loads, JavaScript loads and executes, then data fetching begins, finally rendering with data.
Server Components eliminate this waterfall for initial data, provide better security (no exposed API keys), improve performance (smaller bundles, faster initial load), and enable better SEO since content is server-rendered. However, Client Components are necessary for interactive features that need to refetch data based on user actions, real-time updates, or client-side state changes.
The best practice is to use Server Components for initial data fetching and page rendering, then use Client Components only where you need interactivity, client-side state, or dynamic data updates. You can compose them together, passing server-fetched data as props to client components that handle user interactions.
Code Example:
// SERVER COMPONENT (default in app directory)
// app/dashboard/page.jsx
async function DashboardPage() {
// Fetches on server, before render
// Has access to server-only resources
const userData = await db.user.findUnique({
where: { id: getSessionUserId() }
});
// No loading state needed - data is ready before component renders
return (
<div>
<h1>Welcome, {userData.name}</h1>
{/* Pass data to Client Component for interactive features */}
<InteractiveChart data={userData.stats} />
</div>
);
}
// CLIENT COMPONENT
// components/InteractiveChart.jsx
'use client';
import { useState, useEffect } from 'react';
function InteractiveChart({ data: initialData }) {
const [data, setData] = useState(initialData);
const [loading, setLoading] = useState(false);
// Refetch when user changes filters
const refreshData = async (filters) => {
setLoading(true);
// Fetches in browser, after mount
// Must use public API endpoints (credentials exposed)
const response = await fetch('/api/stats?' + new URLSearchParams(filters));
const newData = await response.json();
setData(newData);
setLoading(false);
};
return (
<div>
{loading ? <Spinner /> : <Chart data={data} />}
<FilterControls onFilterChange={refreshData} />
</div>
);
}
// Comparison Table
/*
┌─────────────────────┬──────────────────────┬──────────────────────â”
│ Aspect │ Server Component │ Client Component │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ When fetches │ Server render time │ After mount (browser)│
│ Syntax │ async/await directly │ useEffect + hooks │
│ Access to │ DB, filesystem, etc │ Public APIs only │
│ Bundle impact │ Zero │ Adds to bundle │
│ Loading state │ Use Suspense │ Manual useState │
│ Refetching │ Page navigation │ User interaction │
│ SEO │ Excellent │ Limited │
└─────────────────────┴──────────────────────┴──────────────────────â”
*/
Mermaid Diagram (if helpful):
sequenceDiagram
participant Browser
participant Server
participant Database
rect rgb(200, 220, 255)
Note over Browser,Database: Server Component Flow
Browser->>Server: Request page
Server->>Database: Fetch data
Database->>Server: Return data
Server->>Server: Render with data
Server->>Browser: Send rendered HTML
end
rect rgb(255, 220, 200)
Note over Browser,Database: Client Component Flow
Browser->>Server: Request page
Server->>Browser: Send HTML + JS bundle
Browser->>Browser: Mount component
Browser->>Server: Fetch data (via API)
Server->>Database: Query
Database->>Server: Return data
Server->>Browser: JSON response
Browser->>Browser: Re-render with data
end
References:
↑ Back to topWhat is the relationship between Server Components and Suspense?
The 30-Second Answer: Server Components automatically integrate with React Suspense—when an async Server Component awaits data, it suspends, triggering the nearest Suspense boundary to show a fallback UI. This enables progressive streaming of content from server to client as data becomes available.
The 2-Minute Answer (If They Want More): Suspense and Server Components form a powerful combination for handling asynchronous rendering. When a Server Component encounters an await statement, it automatically suspends, which signals to React's rendering engine that this component isn't ready yet. The nearest parent Suspense boundary catches this suspension and displays its fallback UI to the user.
This relationship enables streaming server rendering—a technique where the server can start sending HTML to the client before all data is fetched. When React encounters a suspended Server Component, it sends the Suspense fallback HTML immediately, then continues rendering other parts of the page. As suspended components resolve their data, React streams the actual content to replace the fallback in the browser.
This is fundamentally different from traditional server rendering where the entire page waits for all data before sending anything to the client. With Suspense and Server Components, users see something immediately (the fallback), and content progressively appears as it becomes ready. This improves perceived performance significantly.
You can also nest Suspense boundaries to create sophisticated loading experiences. For example, you might have a page-level Suspense for the main content, with additional Suspense boundaries around slower-loading sections like recommendations or comments. Each boundary resolves independently, so fast data appears immediately while slow data shows a loading state.
The key insight is that Suspense isn't just for loading states—it's a fundamental primitive for concurrent rendering that allows React to prioritize and stream different parts of your UI based on data availability.
Code Example:
// app/blog/[slug]/page.jsx
import { Suspense } from 'react';
async function BlogPostPage({ params }) {
return (
<div>
{/* Fast-loading content renders immediately */}
<Header />
{/* Main content suspended until ready */}
<Suspense fallback={<ArticleSkeleton />}>
<Article slug={params.slug} />
</Suspense>
{/* Comments load independently */}
<Suspense fallback={<div>Loading comments...</div>}>
<Comments slug={params.slug} />
</Suspense>
{/* Recommendations can be slowest without blocking */}
<Suspense fallback={<div>Loading recommendations...</div>}>
<Recommendations slug={params.slug} />
</Suspense>
</div>
);
}
// This component suspends when awaiting
async function Article({ slug }) {
// Suspends here - triggers nearest Suspense boundary
const post = await fetch(`https://api.example.com/posts/${slug}`)
.then(res => res.json());
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// This component suspends independently
async function Comments({ slug }) {
// Might take longer - doesn't block Article
const comments = await fetch(`https://api.example.com/posts/${slug}/comments`)
.then(res => res.json());
return (
<div>
<h2>Comments</h2>
{comments.map(comment => (
<Comment key={comment.id} comment={comment} />
))}
</div>
);
}
// Nested Suspense boundaries for complex loading
async function Recommendations({ slug }) {
return (
<div>
<h2>You might also like</h2>
{/* Each recommendation can load independently */}
<Suspense fallback={<RecommendationSkeleton />}>
<RelatedPosts slug={slug} />
</Suspense>
<Suspense fallback={<RecommendationSkeleton />}>
<PopularPosts />
</Suspense>
</div>
);
}
async function RelatedPosts({ slug }) {
const posts = await fetch(`https://api.example.com/posts/${slug}/related`)
.then(res => res.json());
return (
<div>
{posts.map(post => <PostCard key={post.id} post={post} />)}
</div>
);
}
// Demonstrating streaming behavior
// app/streaming-demo/page.jsx
async function StreamingDemo() {
return (
<div>
{/* Instant: No Suspense, no await */}
<div>This appears immediately</div>
{/* Fast: Suspends briefly */}
<Suspense fallback={<div>Loading fast data...</div>}>
<FastComponent />
</Suspense>
{/* Slow: Suspends longer */}
<Suspense fallback={<div>Loading slow data...</div>}>
<SlowComponent />
</Suspense>
</div>
);
}
async function FastComponent() {
// Simulating fast API
await new Promise(resolve => setTimeout(resolve, 100));
return <div>Fast data loaded!</div>;
}
async function SlowComponent() {
// Simulating slow API
await new Promise(resolve => setTimeout(resolve, 3000));
return <div>Slow data loaded!</div>;
}
/*
Timeline of rendering:
0ms: Server sends HTML with "This appears immediately"
and both Suspense fallbacks
100ms: Server streams FastComponent content to replace its fallback
3000ms: Server streams SlowComponent content to replace its fallback
User sees progressive loading without waiting for everything!
*/
Mermaid Diagram (if helpful):
sequenceDiagram
participant Browser
participant Server
participant DB
Browser->>Server: Request page
rect rgb(220, 255, 220)
Note over Server: Immediate Content
Server->>Browser: Send HTML (header + fallbacks)
end
rect rgb(255, 240, 200)
Note over Server,DB: Fast Component
Server->>DB: Fetch fast data
DB->>Server: Return data (100ms)
Server->>Browser: Stream fast content
Browser->>Browser: Replace fallback 1
end
rect rgb(255, 220, 220)
Note over Server,DB: Slow Component
Server->>DB: Fetch slow data
DB->>Server: Return data (3000ms)
Server->>Browser: Stream slow content
Browser->>Browser: Replace fallback 2
end
Note over Browser: Progressive rendering complete!
References:
↑ Back to topServer Components Architecture
What is the RSC payload and what does it contain?
The 30-Second Answer: The RSC payload is a JSON-like streaming format that the server sends to the client containing the serialized representation of Server Components, placeholders for Client Components, and metadata about the component tree. It includes rendered output, component props, module references for Client Components, and streaming instructions that allow the client to progressively reconstruct the UI.
The 2-Minute Answer (If They Want More): The RSC payload is the wire format that bridges server and client in the React Server Components architecture. It's a custom streaming format that represents the component tree in a way that can be sent over the network and reconstructed on the client. Unlike traditional server-side rendering which sends HTML, the RSC payload is a structured representation that the React runtime can interpret.
The payload contains several types of entries: Server Component output (the actual rendered content), Client Component references (module IDs and chunk information for code splitting), serialized props passed to components, and streaming chunks that can be sent incrementally. Each entry is identified by a unique ID that allows the client to piece together the component tree as chunks arrive.
When you look at the actual payload, you'll see rows of JSON-like data prefixed with identifiers. For example, M1 might indicate a module reference for a Client Component bundle, J0 might be a JSON value for props, and S1 could be a Suspense boundary. The format is intentionally compact and streamable, allowing the server to flush content as soon as it's ready rather than waiting for the entire tree to render.
The payload also includes information about lazy-loaded modules, error boundaries, and Suspense boundaries. This allows the client runtime to know which JavaScript bundles to load, where to show loading states, and how to handle errors. The RSC payload is essentially the instructions for the client to reconstruct the exact component tree that was rendered on the server, but with only the Client Components requiring JavaScript execution.
Code Example:
// Server Component that will generate RSC payload
// app/ProductPage.js
import { ClientReview } from './ClientReview'
export default async function ProductPage({ productId }) {
const product = await db.product.findUnique({ where: { id: productId } })
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<ClientReview productId={productId} initialRating={product.rating} />
</div>
)
}
// The RSC payload sent to the client looks something like this:
// (Simplified representation - actual format is more complex)
/*
RSC Payload Format (conceptual):
M1:{"id":"./ClientReview.js","chunks":["client-chunk-123.js"],"name":"ClientReview"}
J0:{"productId":"abc-123","initialRating":4.5}
S1:"<div><h1>Amazing Product</h1><p>This is a great product...</p>"
S1:"</div>"
C1:M1:J0
Breakdown:
- M1: Module reference for ClientReview component
- J0: JSON serialized props {productId, initialRating}
- S1: Server-rendered string chunks (the static HTML parts)
- C1:M1:J0: Client Component instruction - render M1 with props J0
Actual payload (real example from Chrome DevTools):
0:["$","div",null,{"children":[["$","h1",null,{"children":"Amazing Product"}],["$","p",null,{"children":"This is a great product..."}],["$","$L1",null,{"productId":"abc-123","initialRating":4.5}]]}]
1:I{"id":"./ClientReview.js","chunks":["client-chunk-123.js"],"name":"ClientReview"}
*/
// Client-side runtime receives and processes the payload
// This happens automatically in Next.js/React
// Conceptual client-side reconstruction:
function reconstructFromPayload(payload) {
const modules = {} // M1, M2, etc.
const jsonData = {} // J0, J1, etc.
const chunks = [] // S1, S2, etc.
// Parse payload line by line
payload.split('\n').forEach(line => {
if (line.startsWith('M')) {
// Module reference - prepare to load chunk
const [id, moduleInfo] = parseModule(line)
modules[id] = moduleInfo
loadChunk(moduleInfo.chunks) // Lazy load Client Component
} else if (line.startsWith('J')) {
// JSON data - parse and store
const [id, data] = parseJSON(line)
jsonData[id] = data
} else if (line.startsWith('S')) {
// Server-rendered content
chunks.push(parseString(line))
} else if (line.startsWith('C')) {
// Client Component placeholder
const [id, moduleRef, propsRef] = parseClientComponent(line)
return {
type: 'client',
module: modules[moduleRef],
props: jsonData[propsRef]
}
}
})
return reconstructTree(chunks)
}
// Example: Inspecting RSC payload in browser
// Open DevTools -> Network -> Click navigation -> Preview tab
// Look for flight data with content-type: text/x-component
// You can also manually inspect the payload:
async function inspectRSCPayload() {
const response = await fetch('/product/abc-123', {
headers: {
'RSC': '1', // Tells Next.js to return RSC payload instead of HTML
'Next-Router-State-Tree': '%5B%22%22%2C%7B%22children%22%3A...',
}
})
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value)
console.log('RSC Chunk:', chunk)
// Outputs lines like:
// 0:["$","div",null,{"children":...}]
// 1:I{"id":"./ClientReview.js"...}
}
}
Mermaid Diagram:
flowchart LR
A[Server Component Tree] --> B[RSC Renderer]
B --> C[Generate Payload]
C --> D[Module References<br/>M1, M2...]
C --> E[JSON Props<br/>J0, J1...]
C --> F[Server Output<br/>S1, S2...]
C --> G[Client Placeholders<br/>C1, C2...]
D --> H[Stream to Client]
E --> H
F --> H
G --> H
H --> I[Client Runtime]
I --> J[Load Modules]
I --> K[Parse Props]
I --> L[Render Server Output]
I --> M[Hydrate Client Components]
J --> N[Reconstructed UI]
K --> N
L --> N
M --> N
References:
↑ Back to topHow does the React Server Components architecture work?
The 30-Second Answer: React Server Components architecture splits React components into two categories: Server Components that run on the server and render to a special streaming format, and Client Components that hydrate and run in the browser. Server Components can directly access backend resources like databases and APIs without exposing credentials to the client, while Client Components handle interactivity. The server sends a serialized representation of the component tree that the client reconstructs.
The 2-Minute Answer (If They Want More): The RSC architecture fundamentally changes how React applications are structured by introducing a clear boundary between server and client rendering. Server Components execute exclusively on the server during the request, allowing them to perform expensive operations, access databases directly, and read from the filesystem without sending any JavaScript to the client. They render to an intermediate format rather than HTML.
Client Components are marked with the 'use client' directive and work like traditional React components - they ship JavaScript to the browser and can use hooks, event handlers, and browser APIs. The key insight is that Server Components can render Client Components as children, passing serializable props to them. This creates a composable architecture where you nest components based on their capabilities.
During rendering, the server processes the component tree, executing Server Components and marking Client Component boundaries. The output is a streamable payload that contains the rendered Server Component output and instructions for where to place Client Components. The client runtime receives this payload, reconstructs the component tree, and hydrates only the Client Components. This means only the interactive parts of your application ship JavaScript.
The architecture also enables powerful patterns like streaming rendering, where the server can send parts of the UI as they're ready, and automatic code splitting at the Server/Client boundary. Server Components can also refetch and re-render without requiring a full page reload, enabling dynamic updates with server-side data access.
Code Example:
// app/page.js - Server Component (default)
import { db } from '@/lib/database'
import { UserProfile } from './UserProfile' // Client Component
import { ActivityFeed } from './ActivityFeed' // Server Component
// This runs ONLY on the server - no JavaScript sent to client
export default async function DashboardPage({ params }) {
// Direct database access - credentials never exposed
const user = await db.user.findUnique({
where: { id: params.userId },
include: { posts: true, followers: true }
})
// Can do expensive operations server-side
const analytics = await calculateUserAnalytics(user)
return (
<div>
<h1>Dashboard</h1>
{/* Client Component - gets serializable props */}
<UserProfile
user={user}
onFollow={() => {}} // This will error - functions aren't serializable!
/>
{/* Server Component - can be nested */}
<ActivityFeed userId={user.id} />
{/* Server Component with async data fetching */}
<RecommendationsPanel userId={user.id} />
</div>
)
}
// app/UserProfile.js - Client Component
'use client' // This directive marks the boundary
import { useState } from 'react'
export function UserProfile({ user }) {
const [isFollowing, setIsFollowing] = useState(user.isFollowing)
// Client-side interactivity
const handleFollow = async () => {
await fetch(`/api/follow/${user.id}`, { method: 'POST' })
setIsFollowing(true)
}
return (
<div>
<h2>{user.name}</h2>
<button onClick={handleFollow}>
{isFollowing ? 'Following' : 'Follow'}
</button>
</div>
)
}
// app/ActivityFeed.js - Server Component
import { db } from '@/lib/database'
export async function ActivityFeed({ userId }) {
// Each Server Component can fetch its own data
const activities = await db.activity.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
take: 10
})
return (
<div>
{activities.map(activity => (
<ActivityItem key={activity.id} activity={activity} />
))}
</div>
)
}
Mermaid Diagram:
flowchart TD
A[Client Request] --> B[Server: RSC Renderer]
B --> C{Component Type?}
C -->|Server Component| D[Execute on Server]
C -->|Client Component| E[Mark Boundary]
D --> F[Access DB/APIs/Files]
F --> G[Render to RSC Payload]
E --> G
G --> H[Stream to Client]
H --> I[Client: RSC Runtime]
I --> J[Reconstruct Tree]
J --> K{Component Type?}
K -->|Server Component Output| L[Render Output]
K -->|Client Component| M[Hydrate with JS]
L --> N[Final UI]
M --> N
References:
- React Server Components RFC
- Next.js Server Components Documentation
- Understanding React Server Components
How does streaming work with React Server Components?
The 30-Second Answer: Streaming with React Server Components allows the server to send parts of the UI progressively as they become ready, rather than waiting for the entire page to render. Using Suspense boundaries, you can mark sections that might take longer to load, and the server will immediately send the static shell while streaming in dynamic content as it resolves, enabling faster Time to First Byte and progressive page rendering.
The 2-Minute Answer (If They Want More): Streaming fundamentally changes how Server Components deliver content to the browser. Instead of the traditional waterfall where the server must finish rendering everything before sending any bytes, streaming allows the server to flush HTML as soon as parts of the page are ready. This works through React's Suspense boundaries, which mark sections of the tree that might contain async operations.
When the server encounters a Suspense boundary during rendering, it doesn't block the entire response. Instead, it immediately sends everything that's ready, includes a placeholder for the suspended content, and continues rendering in the background. As suspended components resolve (database queries complete, API calls return, etc.), the server streams additional chunks containing the resolved content and JavaScript instructions to swap the placeholders.
The streaming format is built into the RSC payload. The server can send multiple chunks over the same response connection, each containing pieces of the component tree. The client's React runtime receives these chunks incrementally, reconstructs the component tree progressively, and updates the DOM as new content arrives. This happens automatically - you just need to wrap slow components in Suspense boundaries.
Streaming provides several benefits: faster Time to First Byte (users see something immediately), better perceived performance (content loads progressively rather than all-or-nothing), and more efficient resource usage (the server can start processing requests while database queries run). It's particularly powerful for pages with multiple independent data sources, as each can resolve at its own pace without blocking the others.
Code Example:
// ===== BASIC STREAMING PATTERN =====
// app/dashboard/page.js
import { Suspense } from 'react'
import { UserProfile } from './UserProfile'
import { RecentActivity } from './RecentActivity'
import { Analytics } from './Analytics'
export default function DashboardPage() {
return (
<div>
{/* This renders immediately */}
<h1>Dashboard</h1>
<nav>Quick Links</nav>
{/* User profile loads fast, streams first */}
<Suspense fallback={<UserProfileSkeleton />}>
<UserProfile />
</Suspense>
{/* Activity might take longer, streams when ready */}
<Suspense fallback={<ActivitySkeleton />}>
<RecentActivity />
</Suspense>
{/* Analytics is slowest, streams last */}
<Suspense fallback={<AnalyticsSkeleton />}>
<Analytics />
</Suspense>
</div>
)
}
// app/dashboard/UserProfile.js - Fast query (~50ms)
export async function UserProfile() {
// This is cached and fast
const user = await db.user.findUnique({
where: { id: 'current-user' }
})
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
)
}
// app/dashboard/RecentActivity.js - Medium query (~200ms)
export async function RecentActivity() {
// Moderate database query
const activities = await db.activity.findMany({
take: 10,
orderBy: { createdAt: 'desc' }
})
return (
<ul>
{activities.map(activity => (
<li key={activity.id}>{activity.description}</li>
))}
</ul>
)
}
// app/dashboard/Analytics.js - Slow external API (~2s)
export async function Analytics() {
// Slow external analytics API
const data = await fetch('https://analytics-api.com/data', {
cache: 'no-store'
}).then(r => r.json())
return (
<div>
<h2>Analytics</h2>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)
}
// ===== STREAMING TIMELINE =====
/*
0ms: Client receives: <h1>Dashboard</h1><nav>...</nav>
+ 3 loading skeletons
50ms: Stream chunk 1: <UserProfile /> content replaces first skeleton
200ms: Stream chunk 2: <RecentActivity /> content replaces second skeleton
2000ms: Stream chunk 3: <Analytics /> content replaces third skeleton
*/
// ===== NESTED STREAMING =====
// app/ProductPage.js
import { Suspense } from 'react'
export default function ProductPage({ params }) {
return (
<div>
{/* Outer shell streams immediately */}
<Header />
<Suspense fallback={<ProductSkeleton />}>
{/* Main product loads, but has its own suspense boundaries */}
<ProductDetails productId={params.id} />
</Suspense>
<Footer />
</div>
)
}
// app/ProductDetails.js
export async function ProductDetails({ productId }) {
// Fast query for basic product info
const product = await db.product.findUnique({
where: { id: productId }
})
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Nested suspense - reviews load independently */}
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={productId} />
</Suspense>
{/* Related products also stream independently */}
<Suspense fallback={<RelatedSkeleton />}>
<RelatedProducts categoryId={product.categoryId} />
</Suspense>
</div>
)
}
// ===== STREAMING WITH PARALLEL DATA FETCHING =====
// app/OptimizedPage.js
export default function OptimizedPage() {
return (
<div>
<h1>Multiple Data Sources</h1>
{/* All three start fetching in parallel */}
<Suspense fallback={<Skeleton />}>
<ParallelDataLoader />
</Suspense>
</div>
)
}
// app/ParallelDataLoader.js
async function fetchUsers() {
return db.user.findMany() // 100ms
}
async function fetchPosts() {
return db.post.findMany() // 150ms
}
async function fetchComments() {
return fetch('https://api.example.com/comments') // 500ms
.then(r => r.json())
}
export async function ParallelDataLoader() {
// âś… CORRECT: Initiate all fetches before awaiting
const usersPromise = fetchUsers()
const postsPromise = fetchPosts()
const commentsPromise = fetchComments()
// Now await them - they're already running in parallel
const [users, posts, comments] = await Promise.all([
usersPromise,
postsPromise,
commentsPromise
])
// Total time: ~500ms (slowest query)
// vs ~750ms if done sequentially
return (
<div>
<UserList users={users} />
<PostList posts={posts} />
<CommentList comments={comments} />
</div>
)
}
// ===== STREAMING WITH LOADING STATES =====
// app/layout.js - Configure loading behavior
export default function Layout({ children }) {
return (
<html>
<body>
{/* Suspense at layout level for page transitions */}
<Suspense fallback={<GlobalLoader />}>
{children}
</Suspense>
</body>
</html>
)
}
// app/products/loading.js - Next.js special file for instant loading UI
export default function Loading() {
// Automatically wraps page in Suspense with this fallback
return <ProductListSkeleton />
}
// ===== INSPECTING STREAMED RESPONSES =====
// Browser DevTools -> Network -> Document -> Preview tab
// You'll see HTML chunks arriving progressively:
/*
Chunk 1 (immediate):
<!DOCTYPE html>
<html>
<head>...</head>
<body>
<h1>Dashboard</h1>
<div id="suspense-1"><!--$?--><template id="B:0"></template>Loading...</div>
<div id="suspense-2"><!--$?--><template id="B:1"></template>Loading...</div>
Chunk 2 (+50ms):
<div hidden id="S:0"><h2>John Doe</h2><p>john@example.com</p></div>
<script>
document.getElementById('B:0').replaceWith(
document.getElementById('S:0')
)
</script>
Chunk 3 (+200ms):
<div hidden id="S:1"><ul><li>Activity 1</li>...</ul></div>
<script>
document.getElementById('B:1').replaceWith(
document.getElementById('S:1')
)
</script>
*/
// ===== STREAMING BEST PRACTICES =====
// âś… DO: Place Suspense boundaries strategically
function GoodPattern() {
return (
<>
<FastContent />
<Suspense fallback={<Skeleton />}>
<SlowContent />
</Suspense>
</>
)
}
// ❌ DON'T: Wrap everything in one Suspense
function BadPattern() {
return (
<Suspense fallback={<BigSkeleton />}>
<FastContent />
<SlowContent />
</Suspense>
)
// Now FastContent is blocked by SlowContent
}
// âś… DO: Use granular Suspense boundaries
function BetterPattern() {
return (
<>
<Suspense fallback={<Skeleton1 />}>
<Section1 />
</Suspense>
<Suspense fallback={<Skeleton2 />}>
<Section2 />
</Suspense>
<Suspense fallback={<Skeleton3 />}>
<Section3 />
</Suspense>
</>
)
}
Mermaid Diagram:
sequenceDiagram
participant Client
participant Server
participant DB
participant API
Client->>Server: Request /dashboard
activate Server
Server->>Client: Chunk 1: Shell + Suspense placeholders
Note over Client: Renders immediately<br/><h1>, <nav>, skeletons
par Parallel Server Rendering
Server->>DB: Query UserProfile (fast)
Server->>DB: Query RecentActivity (medium)
Server->>API: Fetch Analytics (slow)
end
DB-->>Server: UserProfile data (50ms)
Server->>Client: Chunk 2: UserProfile content
Note over Client: Replaces first skeleton
DB-->>Server: RecentActivity data (200ms)
Server->>Client: Chunk 3: RecentActivity content
Note over Client: Replaces second skeleton
API-->>Server: Analytics data (2000ms)
Server->>Client: Chunk 4: Analytics content
Note over Client: Replaces third skeleton
deactivate Server
Note over Client: Page fully loaded<br/>progressively over 2s
References:
- React Suspense for Data Fetching
- Next.js Streaming and Suspense
- Streaming Server Rendering
- Web.dev: Streaming HTML
Component Composition
What is the "children" pattern for mixing Server and Client Components?
The 30-Second Answer: The children pattern involves passing Server Components as the children prop to Client Components, allowing you to create interactive wrappers around server-rendered content. This works because React doesn't evaluate children until render time, so the Client Component receives pre-rendered server content without importing it directly.
The 2-Minute Answer (If They Want More): The children pattern is one of the most powerful composition techniques in the Server Components architecture. It solves a fundamental problem: how do you wrap server-rendered content with client-side interactivity when Client Components cannot import Server Components?
The pattern works because of React's composition model. When you pass JSX as children to a component, React doesn't immediately evaluate that JSX—it passes it as a prop. The parent component that renders the Client Component (which is a Server Component) evaluates the children on the server, and the result is serialized and sent to the client. The Client Component then receives these pre-rendered elements and can position them in its output.
This pattern is essential for common UI patterns like tabs, accordions, modals, and sidebars where you want the wrapper to be interactive (client-side) but the content to be server-rendered for performance and data access. For example, you might have a client-side tab component that manages selected state, but each tab's content is a Server Component that fetches its own data.
The key advantage is that you get the best of both worlds: client-side interactivity for the wrapper and server-side rendering for the content, including the ability to fetch data, access databases, and use server-only APIs within the children.
Code Example:
// app/layout.js (Server Component)
import ClientSidebar from './ClientSidebar'
import ServerNav from './ServerNav'
import ServerUserProfile from './ServerUserProfile'
export default function Layout({ children }) {
return (
<html>
<body>
{/* Client Component wraps Server Components via children pattern */}
<ClientSidebar>
{/* These Server Components are evaluated on the server */}
<ServerNav />
<ServerUserProfile />
</ClientSidebar>
<main>{children}</main>
</body>
</html>
)
}
// ClientSidebar.js
'use client'
import { useState } from 'react'
export default function ClientSidebar({ children }) {
const [isOpen, setIsOpen] = useState(true)
return (
<aside className={isOpen ? 'open' : 'closed'}>
<button onClick={() => setIsOpen(!isOpen)}>
Toggle Sidebar
</button>
{/* Server Components rendered here without importing them */}
{isOpen && <div className="sidebar-content">{children}</div>}
</aside>
)
}
// ServerNav.js (Server Component)
export default async function ServerNav() {
// Server-side data fetching
const navItems = await fetchNavigation()
return (
<nav>
{navItems.map(item => (
<a key={item.id} href={item.url}>{item.label}</a>
))}
</nav>
)
}
// ServerUserProfile.js (Server Component)
import { cookies } from 'next/headers'
export default async function ServerUserProfile() {
// Access server-only APIs
const session = cookies().get('session')
const user = await fetchUser(session)
return (
<div className="profile">
<img src={user.avatar} alt={user.name} />
<p>{user.name}</p>
</div>
)
}
Mermaid Diagram (if helpful):
flowchart TD
A[Layout - Server Component] -->|renders with children| B[ClientSidebar]
A -->|evaluates on server| C[ServerNav]
A -->|evaluates on server| D[ServerUserProfile]
C -->|pre-rendered| E[Serialized JSX]
D -->|pre-rendered| E
E -->|passed as children prop| B
B -->|client state isOpen| F[Toggle Interaction]
B -->|renders| E
style A fill:#e1f5ff
style C fill:#e1f5ff
style D fill:#e1f5ff
style B fill:#ffe1e1
style F fill:#ffe1e1
References:
↑ Back to topPerformance
What is zero-bundle-size components and how do Server Components achieve it?
The 30-Second Answer: Zero-bundle-size components are React components whose code and dependencies never get included in the client JavaScript bundle. Server Components achieve this by executing entirely on the server and sending only their rendered output (RSC payload) to the client, allowing unlimited use of heavy libraries without affecting bundle size.
The 2-Minute Answer (If They Want More): Zero-bundle-size is one of the most powerful features of Server Components. In traditional React applications, every component you import adds to your bundle size—including all its dependencies. If you use a 200KB date formatting library in ten components, that 200KB ships to every user. With Server Components, this entire cost disappears from the client bundle.
Server Components achieve zero-bundle-size through their execution model. They run exclusively on the server during the render phase and produce a special serialized format called the RSC payload. This payload contains the rendered output (elements, props, and component boundaries) but not the component code itself. When the client receives this payload, React reconciles it with the Client Component tree without needing the Server Component source code.
This enables patterns previously impossible in client-side React. You can import massive libraries like Prisma ORM, GraphQL clients, image processing libraries, or entire CMS SDKs directly in your components without any bundle impact. These dependencies execute on the server, process your data, and the Server Component returns plain data structures that serialize to the client.
The "zero" is literal: the component code, its imports, and all server-side dependencies contribute 0 bytes to the client bundle. Only Client Components (marked with 'use client') and their dependencies affect bundle size. This creates a clear mental model: Server Components for data and rendering, Client Components for interactivity.
Code Example:
// Server Component - ZERO bundle impact
import Database from 'prisma'; // ~5MB library
import { processImage } from 'sharp'; // ~8MB library
import { parseMarkdown } from 'markdown-it'; // ~500KB library
import ComplexChart from 'complex-charts'; // ~1MB library
export default async function ProductPage({ id }) {
// All these heavy operations happen server-side
const product = await Database.product.findUnique({
where: { id },
include: { reviews: true, analytics: true }
});
const optimizedImage = await processImage(product.image)
.resize(800, 600)
.toFormat('webp')
.toBuffer();
const description = parseMarkdown(product.description);
// Component code and libraries never sent to client
return (
<div>
<img src={`data:image/webp;base64,${optimizedImage.toString('base64')}`} />
<div dangerouslySetInnerHTML={{ __html: description }} />
<ComplexChart data={product.analytics} />
</div>
);
}
// Client bundle size for this page: ~0KB from Server Components
// Only framework runtime and Client Components count
// Client Component - DOES affect bundle
'use client';
import { useState } from 'react'; // Small, tree-shaken
import { trackEvent } from 'analytics'; // ~20KB
export function AddToCart({ productId }) {
const [isAdding, setIsAdding] = useState(false);
const handleAdd = async () => {
setIsAdding(true);
await trackEvent('add_to_cart', { productId });
setIsAdding(false);
};
return <button onClick={handleAdd}>Add to Cart</button>;
}
// Bundle size: Only AddToCart + dependencies (~25KB total)
Mermaid Diagram:
graph TB
subgraph Server
A[Server Component] --> B[Import Heavy Libraries]
B --> C[Prisma 5MB]
B --> D[Sharp 8MB]
B --> E[Markdown-it 500KB]
C --> F[Execute & Render]
D --> F
E --> F
F --> G[RSC Payload JSON]
end
subgraph Client
G --> H[React Runtime]
H --> I[Reconcile Payload]
I --> J[Rendered UI]
K[Client Components] --> L[React 45KB]
K --> M[App Code 30KB]
L --> J
M --> J
end
N[Client Bundle Size] --> O[Client Components Only: 75KB]
P[Server Dependencies] --> Q[Not in Bundle: 13.5MB]
style G fill:#90EE90
style O fill:#90EE90
style Q fill:#ADD8E6
References:
↑ Back to topServer Actions
What is progressive enhancement with Server Actions?
The 30-Second Answer:
Progressive enhancement with Server Actions means forms work even before JavaScript loads or if it fails. By using the native form action attribute with Server Actions, forms submit traditionally via POST requests, then upgrade to client-side transitions once React hydrates.
The 2-Minute Answer (If They Want More): Progressive enhancement is a web design principle where basic functionality works without JavaScript, then enhanced features are added when JavaScript is available. Server Actions embrace this by working seamlessly with native HTML forms.
When you set a Server Action as a form's action attribute, the form submits to the server using a traditional POST request even without JavaScript. Once React hydrates on the client, the same form automatically upgrades to use client-side transitions, providing a smooth single-page application experience with optimistic updates and pending states.
This approach provides the best of both worlds: reliability and accessibility (works for users with disabled JavaScript, slow networks, or screen readers) plus modern UX features like instant feedback and optimistic updates when JavaScript is available.
To implement progressive enhancement, use Server Actions directly with form actions, provide meaningful HTML form validation attributes, ensure buttons have proper disabled states, and consider displaying loading states that work both with and without JavaScript.
Code Example:
// app/actions/comments.js
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { db } from '@/lib/database'
export async function addComment(formData) {
const postId = formData.get('postId')
const content = formData.get('content')
const author = formData.get('author')
// Server-side validation
if (!content || content.length < 1) {
return { error: 'Content is required' }
}
if (!author || author.length < 2) {
return { error: 'Author name must be at least 2 characters' }
}
try {
await db.comments.create({
data: {
postId,
content,
author,
}
})
revalidatePath(`/posts/${postId}`)
// Works with or without JS - redirects after submission
redirect(`/posts/${postId}#comments`)
} catch (error) {
return { error: 'Failed to add comment' }
}
}
// app/posts/[id]/CommentForm.jsx
'use client'
import { addComment } from '@/app/actions/comments'
import { useFormState, useFormStatus } from 'react-dom'
// Submit button with progressive enhancement
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{/* Works without JS - shows "Post Comment" */}
{/* With JS - shows pending state */}
{pending ? 'Posting...' : 'Post Comment'}
</button>
)
}
export default function CommentForm({ postId }) {
const [state, formAction] = useFormState(addComment, { error: null })
return (
<form action={formAction}>
{/* Hidden field - works with traditional POST */}
<input type="hidden" name="postId" value={postId} />
<div>
<label htmlFor="author">Name:</label>
<input
id="author"
name="author"
type="text"
required
minLength={2}
placeholder="Your name"
/>
</div>
<div>
<label htmlFor="content">Comment:</label>
<textarea
id="content"
name="content"
required
minLength={1}
rows={4}
placeholder="Share your thoughts..."
/>
</div>
{/* Error message - works with both JS and non-JS */}
{state?.error && (
<p className="error" role="alert">
{state.error}
</p>
)}
<SubmitButton />
</form>
)
}
// Alternative: Pure Server Component version (no JS needed)
// app/posts/[id]/BasicCommentForm.jsx
import { addComment } from '@/app/actions/comments'
export default function BasicCommentForm({ postId }) {
return (
<form action={addComment}>
<input type="hidden" name="postId" value={postId} />
<div>
<label htmlFor="author">Name:</label>
<input
id="author"
name="author"
type="text"
required
minLength={2}
/>
</div>
<div>
<label htmlFor="content">Comment:</label>
<textarea
id="content"
name="content"
required
rows={4}
/>
</div>
<button type="submit">Post Comment</button>
</form>
)
}
Mermaid Diagram (if helpful):
flowchart TD
A[Form Rendered] --> B{JavaScript Loaded?}
B -->|No| C[Traditional POST Request]
C --> D[Server Action Executes]
D --> E[Full Page Refresh/Redirect]
B -->|Yes| F[React Hydration]
F --> G[Enhanced Form Submission]
G --> D
D --> H[Client-Side Transition]
H --> I[Optimistic Updates & Pending States]
References:
- Progressive Enhancement Principles
- Next.js Server Actions Progressive Enhancement
- React Server Actions with Forms
Framework Integration
What is the default component type in Next.js App Router?
The 30-Second Answer:
All components in the Next.js App Router (app/ directory) are Server Components by default. You must explicitly opt into Client Components by adding the 'use client' directive at the top of a file, making the server-first approach the default paradigm.
The 2-Minute Answer (If They Want More): The architectural decision to make Server Components the default in Next.js App Router represents a significant shift in mental model. Unlike the Pages Router where everything was client-rendered (or SSR with hydration), the App Router assumes components should run on the server unless they need client-side interactivity. This default encourages better performance by keeping more code and data fetching on the server.
This default means that when you create any .js, .jsx, .ts, or .tsx file in the app/ directory—whether it's a page, layout, component, or any other file—it will execute as a Server Component unless marked otherwise. Server Components can be async, directly access backend resources, use Node.js APIs, and import server-only modules without sending that code to the client.
The 'use client' directive creates a boundary in your component tree. Everything below that directive (the component and its children) becomes part of the client bundle and can use React hooks, event handlers, browser APIs, and lifecycle methods. However, you can still pass Server Components as children to Client Components through composition, maintaining the performance benefits.
This default aligns with React's vision for the future: server-first rendering with strategic client-side interactivity. It naturally guides developers toward better performance practices—you only pay the cost of client JavaScript for components that truly need it, while data fetching, authentication checks, and heavy computations stay on the server.
Code Example:
// app/dashboard/page.js
// This is a Server Component by default - no directive needed
import { db } from '@/lib/database';
import ClientChart from './ClientChart';
async function getDashboardData() {
// Direct database access - server-only code
const data = await db.query('SELECT * FROM analytics');
return data;
}
export default async function DashboardPage() {
const data = await getDashboardData();
return (
<div>
<h1>Dashboard</h1>
{/* This component needs to be a Client Component */}
<ClientChart data={data} />
</div>
);
}
// app/dashboard/ClientChart.js
'use client'; // Explicitly opt into Client Component
import { useState, useEffect } from 'react';
import { Chart } from 'chart.js';
export default function ClientChart({ data }) {
const [chart, setChart] = useState(null);
useEffect(() => {
// Browser API access - only works in Client Components
const ctx = document.getElementById('myChart');
const newChart = new Chart(ctx, { /* config */ });
setChart(newChart);
return () => newChart.destroy();
}, []);
return <canvas id="myChart" />;
}
// app/components/ServerList.js
// Server Component - can use server-only imports
import { getServerSession } from 'next-auth/next';
import { headers } from 'next/headers';
export default async function ServerList() {
// Access request headers - server-only
const headersList = headers();
const session = await getServerSession();
return (
<div>
<p>User: {session?.user?.name}</p>
{/* Server-rendered content */}
</div>
);
}
// Attempting to use client features in Server Component causes error
// app/wrong-example.js
// This will ERROR because it's a Server Component by default
export default function WrongExample() {
const [count, setCount] = useState(0); // ❌ Error: useState not available
useEffect(() => {}, []); // ❌ Error: useEffect not available
return <button onClick={() => {}}> // ❌ Error: onClick handler not available
Click me
</button>;
}
// Correct version - add 'use client'
// app/correct-example.js
'use client';
export default function CorrectExample() {
const [count, setCount] = useState(0); // âś… Works now
return <button onClick={() => setCount(count + 1)}>
Count: {count}
</button>;
}
Mermaid Diagram:
flowchart TD
A[Component in app/ directory] --> B{Has 'use client'?}
B -->|No| C[Server Component Default]
B -->|Yes| D[Client Component]
C --> E[Can use async/await]
C --> F[Direct backend access]
C --> G[No JavaScript to client]
D --> H[Can use hooks]
D --> I[Event handlers]
D --> J[Browser APIs]
style C fill:#90EE90
style D fill:#FFB6C1
References:
↑ Back to topCommon Patterns and Best Practices
What are common mistakes when working with React Server Components?
The 30-Second Answer: Common mistakes include using 'use client' too liberally, trying to pass non-serializable props (functions, dates, class instances) from Server to Client Components, importing Server Components into Client Components, and forgetting that Server Components can't use browser APIs or React hooks.
The 2-Minute Answer (If They Want More): The most frequent mistake is adding 'use client' at the top level too quickly. Developers coming from traditional React often mark entire pages as Client Components when only a small interactive piece needs client-side JavaScript. This defeats the purpose of Server Components and bloats the bundle. Always start with Server Components and only add 'use client' to the smallest component that needs interactivity.
Another critical error is prop serialization issues. Server Components can only pass serializable data to Client Components—plain objects, arrays, strings, numbers, booleans. You can't pass functions, class instances, Date objects, or components with closures. This often catches developers off guard when they try to pass event handlers or complex objects down the tree.
Many developers also struggle with the import boundary. Client Components can't import Server Components, but they can receive them as children or props. This means you need to restructure your component composition to pass Server Components through the React children pattern rather than importing them directly.
Finally, there's confusion about async/await usage. Server Components can be async functions, but Client Components cannot. Trying to make a Client Component async or using top-level await will cause errors. Data fetching should happen in Server Components, with results passed as props to Client Components that need them.
Code Example:
// ❌ MISTAKE 1: Using 'use client' too high in the tree
'use client';
// This makes EVERYTHING below client-side rendered
export default function Dashboard() {
const data = await fetchData(); // Error: Can't use await in Client Component
return (
<div>
<Header />
<Sidebar />
<MainContent data={data} />
<Footer />
</div>
);
}
// âś… CORRECT: Keep Server Component, extract interactive parts
export default async function Dashboard() {
const data = await fetchData(); // âś“ Works in Server Component
return (
<div>
<Header />
<Sidebar />
<MainContent data={data} />
<Footer />
</div>
);
}
// Only the interactive piece is a Client Component
'use client';
export function InteractiveChart({ data }) {
const [filter, setFilter] = useState('all');
// ... interactive logic
}
// ❌ MISTAKE 2: Passing non-serializable props
// app/page.tsx (Server Component)
export default async function Page() {
const data = await fetchData();
const handleClick = () => console.log('clicked'); // Function
const createdAt = new Date(); // Date object
const userClass = new User('john'); // Class instance
return (
<ClientComponent
onClick={handleClick} // ❌ Can't serialize functions
timestamp={createdAt} // ❌ Can't serialize Date objects
user={userClass} // ❌ Can't serialize class instances
/>
);
}
// âś… CORRECT: Serialize data, move handlers to Client Component
export default async function Page() {
const data = await fetchData();
return (
<ClientComponent
timestamp={data.createdAt.toISOString()} // âś“ Serialize to string
userId={data.user.id} // âś“ Pass primitive values
userName={data.user.name} // âś“ Plain data
/>
);
}
'use client';
export function ClientComponent({ timestamp, userId, userName }) {
// Define handlers in the Client Component
const handleClick = () => console.log('clicked', userId);
const date = new Date(timestamp); // Reconstruct Date client-side
return <button onClick={handleClick}>{userName}</button>;
}
// ❌ MISTAKE 3: Importing Server Component into Client Component
'use client';
import { ServerDataComponent } from './ServerDataComponent'; // ❌ Error!
export function ClientWrapper() {
return <ServerDataComponent />; // Can't import Server into Client
}
// âś… CORRECT: Use children pattern
'use client';
export function ClientWrapper({ children }) {
const [isOpen, setIsOpen] = useState(true);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && children} {/* Server Component passed as children */}
</div>
);
}
// In parent Server Component
export default async function Page() {
return (
<ClientWrapper>
<ServerDataComponent /> {/* âś“ Passed as children */}
</ClientWrapper>
);
}
// ❌ MISTAKE 4: Async Client Components
'use client';
export default async function ClientPage() { // ❌ Client Components can't be async
const data = await fetch('/api/data');
return <div>{data}</div>;
}
// âś… CORRECT: Fetch in Server Component or use useEffect
'use client';
import { useEffect, useState } from 'react';
export default function ClientPage({ initialData }) {
const [data, setData] = useState(initialData);
useEffect(() => {
// Client-side fetching if needed
fetch('/api/data').then(res => res.json()).then(setData);
}, []);
return <div>{data}</div>;
}
// ❌ MISTAKE 5: Using browser APIs in Server Components
export default async function Page() {
const theme = localStorage.getItem('theme'); // ❌ localStorage doesn't exist on server
const width = window.innerWidth; // ❌ window doesn't exist on server
return <div className={theme}>Width: {width}</div>;
}
// âś… CORRECT: Use Client Component for browser APIs
'use client';
import { useEffect, useState } from 'react';
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
useEffect(() => {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) setTheme(savedTheme);
}, []);
return <div className={theme}>{children}</div>;
}
Common Mistakes Checklist:
| Mistake | Why It's Wrong | Solution |
|---|---|---|
| Adding 'use client' at page level | Entire page becomes client-side | Push 'use client' to leaf components |
| Passing functions as props | Functions can't be serialized | Define handlers in Client Component |
| Passing Date/class instances | Not JSON-serializable | Convert to strings/plain objects |
| Importing Server into Client | Breaks boundary rules | Use children/props pattern |
| Making Client Component async | Not supported | Fetch in Server Component or useEffect |
| Using browser APIs in Server | APIs don't exist on server | Move to Client Component |
| Forgetting 'use client' directive | Component treated as Server | Add directive when needed |
| Over-fetching in components | Multiple waterfalls | Fetch at highest level possible |
References:
- Server Component Patterns
- Data Fetching Patterns and Best Practices
- Common React Server Components Pitfalls
State and Interactivity
Can Server Components have state using useState or useReducer?
The 30-Second Answer: No, Server Components cannot use useState or useReducer because they render once on the server and don't have a lifecycle on the client. State management hooks require client-side reactivity, so you must use Client Components (marked with 'use client') for any stateful logic.
The 2-Minute Answer (If They Want More): Server Components are fundamentally different from traditional React components because they execute only on the server during the rendering process. They don't hydrate on the client, which means they have no client-side lifecycle and cannot respond to user interactions or maintain state over time.
When you need state management, you must create a Client Component by adding the 'use client' directive at the top of the file. This tells React to hydrate this component on the client, enabling access to all React hooks including useState, useReducer, useEffect, and event handlers.
The recommended pattern is to use Server Components as the default and strategically place Client Components at the leaves of your component tree where interactivity is needed. This allows you to benefit from server rendering for the majority of your UI while keeping interactive portions on the client.
You can compose Server and Client Components together, passing server-rendered content to Client Components as children or props. This hybrid approach maximizes the benefits of both rendering strategies.
Code Example:
// ❌ This will NOT work - Server Component cannot use state
export default async function ServerComponent() {
const [count, setCount] = useState(0); // Error!
return <div>Count: {count}</div>;
}
// âś… Correct approach - Use Client Component for state
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
// âś… Best practice - Server Component wraps Client Component
// app/page.js (Server Component)
import Counter from './Counter';
export default async function Page() {
// Server-side data fetching
const data = await fetch('https://api.example.com/data').then(r => r.json());
return (
<div>
<h1>Server-rendered heading</h1>
<p>Server data: {data.title}</p>
{/* Client Component for interactivity */}
<Counter />
</div>
);
}
Mermaid Diagram (if helpful):
flowchart TD
A[Server Component] -->|Renders on server| B[Static HTML]
A -->|Cannot use| C[useState/useReducer]
D['use client' Component] -->|Hydrates on client| E[Interactive Component]
D -->|Can use| F[All React Hooks]
A -->|Can import & render| D
B -->|Sent to browser| G[Client]
E -->|Runs in| G
References:
↑ Back to topMigration and Adoption
How do you migrate an existing React application to use Server Components?
The 30-Second Answer: I start by gradually introducing Server Components in a Next.js App Router project, keeping existing components as Client Components by default. I identify data-fetching components that don't need interactivity, convert them to Server Components, and progressively move the Client Component boundary down the component tree to minimize the client-side JavaScript bundle.
The 2-Minute Answer (If They Want More): Migration to Server Components requires a strategic, incremental approach rather than a complete rewrite. The first step is setting up a framework that supports RSC, typically Next.js 13+ with the App Router. I begin by wrapping my existing application in Server Components as the default, explicitly marking interactive components with the 'use client' directive.
The key is identifying components by their responsibilities. Data-fetching components that render static content are ideal candidates for Server Components. I extract server-side data fetching from useEffect hooks and place it directly in Server Components. Components requiring hooks like useState, useEffect, or event handlers must remain Client Components.
I follow a top-down migration strategy: start with layout and page-level components as Server Components, then progressively push the Client Component boundary down to leaf components. This maximizes the benefits of server rendering while maintaining interactivity where needed. Shared components used in both contexts need careful consideration - I often split them into separate Server and Client versions or use composition patterns.
Critical considerations include handling routing (Next.js App Router vs Pages Router), managing state (moving from client-side state to server-side data fetching), and updating data fetching patterns (from useEffect to async Server Components). I also need to audit third-party libraries for client-side dependencies and replace or wrapper them appropriately.
Code Example:
// BEFORE: Traditional Client Component with data fetching
'use client';
import { useState, useEffect } from 'react';
function ProductList() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(setProducts);
}, []);
return (
<div>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// AFTER: Migration to Server Component + Client Component pattern
// app/products/page.tsx (Server Component)
async function ProductList() {
// Data fetching happens on the server
const products = await fetch('https://api.example.com/products', {
cache: 'no-store' // or other caching strategies
}).then(res => res.json());
return (
<div>
{products.map(product => (
// Server Component rendering non-interactive content
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// components/ProductCard.tsx (Server Component for static parts)
function ProductCard({ product }) {
return (
<div className="product-card">
<h2>{product.name}</h2>
<p>{product.description}</p>
<p>${product.price}</p>
{/* Only the interactive button is a Client Component */}
<AddToCartButton productId={product.id} />
</div>
);
}
// components/AddToCartButton.tsx (Client Component - minimal interactivity)
'use client';
import { useState } from 'react';
function AddToCartButton({ productId }) {
const [isAdding, setIsAdding] = useState(false);
const handleClick = async () => {
setIsAdding(true);
await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify({ productId })
});
setIsAdding(false);
};
return (
<button onClick={handleClick} disabled={isAdding}>
{isAdding ? 'Adding...' : 'Add to Cart'}
</button>
);
}
// Migration checklist for each component:
// 1. Does it need interactivity? → Keep as Client Component
// 2. Does it fetch data? → Convert to async Server Component
// 3. Does it use browser APIs? → Keep as Client Component
// 4. Is it purely presentational? → Convert to Server Component
// 5. Can you split it? → Extract Client parts, keep Server wrapper
Mermaid Diagram (if helpful):
flowchart TD
A[Start Migration] --> B{Assess Component}
B -->|Interactive/Hooks| C[Mark 'use client']
B -->|Data Fetching| D[Convert to Server Component]
B -->|Mixed| E[Split Component]
D --> F[Move fetch to Server]
F --> G[Remove useEffect]
G --> H[Make async]
E --> I[Server Wrapper]
E --> J[Client Leaf]
I --> J
C --> K[Keep existing code]
H --> L[Optimize Bundle]
J --> L
K --> L
L --> M{More Components?}
M -->|Yes| B
M -->|No| N[Complete Migration]
style C fill:#ff9999
style D fill:#99ff99
style E fill:#ffff99
References:
- Next.js App Router Migration Guide
- React Server Components RFC
- Patterns for Building with Server Components