React & Modern Frontend Frameworks
Server Components & Next.js Architecture
React Server Components (RSC) represent the biggest architectural shift in React since hooks. Understanding this model is increasingly tested in interviews, especially at companies using Next.js.
The Server Components Mental Model
React now has two types of components:
| Server Components | Client Components | |
|---|---|---|
| Where they run | Server only | Server (SSR) + Client (hydration) |
| Can use | async/await, database, fs | useState, useEffect, event handlers |
| Cannot use | Hooks, browser APIs, event handlers | Direct DB access, fs |
| Bundle impact | Zero — code stays on server | Included in JS bundle |
| Default in Next.js App Router | Yes (default) | Must add "use client" |
// Server Component (default in App Router) — no directive needed
async function ProductPage({ params }) {
// This code runs ONLY on the server
const product = await db.products.findById(params.id);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Client Component for interactive parts */}
<AddToCartButton productId={product.id} price={product.price} />
</div>
);
}
// Client Component — must have "use client" directive
'use client';
import { useState } from 'react';
function AddToCartButton({ productId, price }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => {
addToCart(productId);
setAdded(true);
}}>
{added ? 'Added!' : `Add to Cart — $${price}`}
</button>
);
}
When to Use Each
Keep as Server Component when:
- Fetching data from a database or API
- Accessing backend resources (file system, environment variables)
- Rendering static or infrequently updated content
- The component has no interactivity
Convert to Client Component when:
- Using React hooks (useState, useEffect, useRef, etc.)
- Adding event handlers (onClick, onChange, onSubmit)
- Using browser-only APIs (localStorage, window, navigator)
- The component needs to respond to user interaction
Best practice: Push the "use client" boundary as far down the tree as possible. Keep parent layouts as Server Components and only make the interactive leaf components client-side.
Next.js App Router Patterns
Data Fetching
// Server Component: fetch directly (no useEffect needed)
async function BlogPosts() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // ISR: revalidate every hour
}).then(r => r.json());
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Server Actions
// Server Action: defined with "use server"
async function createPost(formData) {
'use server';
const title = formData.get('title');
const content = formData.get('content');
await db.posts.create({ title, content });
revalidatePath('/posts');
}
// Used in a form (Server Component)
function NewPostForm() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="content" required />
<button type="submit">Create Post</button>
</form>
);
}
Streaming SSR
import { Suspense } from 'react';
// The shell renders immediately, slow parts stream in
async function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* Renders immediately */}
<Suspense fallback={<Skeleton />}>
{/* Streams in when data is ready */}
<RevenueChart />
</Suspense>
<Suspense fallback={<Skeleton />}>
<RecentOrders />
</Suspense>
</div>
);
}
Rendering Strategies
| Strategy | When Content Updates | Use Case |
|---|---|---|
| SSG (Static) | At build time only | Marketing pages, docs |
| ISR (Incremental) | Periodically (revalidate timer) | Blog posts, product pages |
| SSR (Dynamic) | Every request | Personalized content, real-time data |
| Client-side | After hydration | Highly interactive features |
// SSG: no fetch options (default)
const data = await fetch('https://api.example.com/static');
// ISR: revalidate every 60 seconds
const data = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 }
});
// SSR: fresh on every request
const data = await fetch('https://api.example.com/dashboard', {
cache: 'no-store'
});
Common Interview Questions
Q: "Can a Server Component import a Client Component?"
Yes. Server Components can render Client Components as children. The Client Component just needs the "use client" directive.
Q: "Can a Client Component import a Server Component?"
Not directly. But a Client Component can accept Server Components as children via the children prop pattern.
Q: "What happens to a Server Component's code in the browser?" Nothing — it never reaches the browser. Server Component code is not included in the JavaScript bundle. This is the key performance benefit.
Interview tip: The ability to explain the RSC boundary — why certain code runs on the server vs. client, and how data flows between them — is a differentiator. Most candidates can use hooks but few can articulate the architectural model.
Now let's apply what you've learned by building a searchable data table in the next lab. :::