React & Modern Frontend Frameworks

Server Components & Next.js Architecture

4 min read

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. :::

Quiz

Module 3: React & Modern Frontend Frameworks

Take Quiz