Skip to main content

What are Server Components?

React Server Components (RSC) are a new paradigm for building React applications. In NextJS App Router, all components are Server Components by default.
Server Components render exclusively on the server (or at build time) and never ship JavaScript to the client.

Key Benefits

Zero Bundle Size

Server Components don’t add to client JavaScript bundle

Direct Backend Access

Access databases, file systems, and APIs directly

Better Performance

Less JavaScript to download and parse

Automatic Code Splitting

Only client components are split and loaded

Server vs Client Components

Let’s examine the difference with real examples from the course:

Server Component Example

components/RSCDemo.js
export default async function RSCDemo() {
  console.log('RSCDemo rendered');
  return (
    <div className="rsc">
      <h2>A React Server Component</h2>
      <p>
        Will <strong>ONLY</strong> be rendered on the server or at build time.
      </p>
      <p>
        <strong>NEVER</strong> on the client-side!
      </p>
    </div>
  );
}
  • Can be async functions
  • Can access backend resources directly
  • Cannot use React hooks (useState, useEffect, etc.)
  • Cannot use browser APIs
  • Render once on the server
  • Console logs appear in terminal, not browser

Client Component Example

components/ClientDemo.js
'use client';

import { useState } from 'react';

export default function ClientDemo({ children }) {
  const [count, setCount] = useState(0); // <- this is why it's a client component

  console.log('ClientDemo rendered');
  return (
    <div className="client-cmp">
      <h2>A React Client Component</h2>
      <p>
        Will be rendered on the client <strong>AND</strong> the server.
      </p>
      {children}
    </div>
  );
}
The 'use client' directive must be at the top of the file before any imports.

When to Use Each

Use Server Components for:

1

Data Fetching

Fetch data directly from databases or APIs
2

Static Content

Display content that doesn’t need interactivity
3

Backend Logic

Access server-only resources (file system, secrets)
4

Large Dependencies

Keep heavy libraries on the server

Use Client Components for:

1

Interactivity

Event handlers (onClick, onChange, etc.)
2

State Management

useState, useReducer, useContext
3

Effects

useEffect, useLayoutEffect
4

Browser APIs

localStorage, window, document

Composing Server and Client Components

You can nest Server Components inside Client Components, but only as props:
app/page.js
import ClientDemo from '@/components/ClientDemo';
import RSCDemo from '@/components/RSCDemo';

export default function Home() {
  return (
    <main>
      <ClientDemo>
        <RSCDemo />
      </ClientDemo>
    </main>
  );
}
Pass Server Components as children or props to Client Components. This preserves the server-only nature of the nested component.

Data Fetching with Server Components

Server Components can be async and fetch data directly:
components/DataFetchingDemo.js
import fs from 'node:fs/promises';

export default async function DataFetchingDemo() {
  const data = await fs.readFile('dummy-db.json', 'utf-8');
  const users = JSON.parse(data);

  return (
    <div className="rsc">
      <h2>RSC with Data Fetching</h2>
      <p>
        Uses <strong>async / await</strong> for data fetching.
      </p>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            {user.name} ({user.title})
          </li>
        ))}
      </ul>
    </div>
  );
}
Notice how we import fs from Node.js directly! This code only runs on the server, so we have full access to server-side APIs.

Real-World Example: Meals Page

Let’s look at a complete example from the Foodies app:
app/meals/page.js
import Link from 'next/link';
import classes from './page.module.css';
import MealsGrid from '@/components/meals/meals-grid';
import { getMeals } from '@/lib/meals';

export default async function MealsPage() {
  const meals = await getMeals();

  return (
    <>
      <header className={classes.header}>
        <h1>
          Delicious meals, created{' '}
          <span className={classes.highlight}>by you</span>
        </h1>
        <p>
          Choose your favorite recipe and cook it yourself. It is easy and fun!
        </p>
        <p className={classes.cta}>
          <Link href="/meals/share">
            Share Your Favorite Recipe
          </Link>
        </p>
      </header>
      <main className={classes.main}>
        <MealsGrid meals={meals} />
      </main>
    </>
  );
}
  1. No 'use client' directive
  2. Declared as async function
  3. Directly calls getMeals() which queries a database
  4. No interactivity or browser APIs needed
  5. Renders once on server, sent as HTML to client

Streaming with Suspense

Server Components work seamlessly with React Suspense for progressive rendering:
app/meals/page.js
import { Suspense } from 'react';
import Link from 'next/link';
import classes from './page.module.css';
import MealsGrid from '@/components/meals/meals-grid';
import { getMeals } from '@/lib/meals';

async function Meals() {
  const meals = await getMeals();
  return <MealsGrid meals={meals} />;
}

export default function MealsPage() {
  return (
    <>
      <header className={classes.header}>
        <h1>
          Delicious meals, created{' '}
          <span className={classes.highlight}>by you</span>
        </h1>
        <p>
          Choose your favorite recipe and cook it yourself. It is easy and fun!
        </p>
        <p className={classes.cta}>
          <Link href="/meals/share">Share Your Favorite Recipe</Link>
        </p>
      </header>
      <main className={classes.main}>
        <Suspense fallback={<p className={classes.loading}>Fetching meals...</p>}>
          <Meals />
        </Suspense>
      </main>
    </>
  );
}
1

Instant Shell

Page shell renders immediately
2

Show Fallback

Suspense boundary displays loading state
3

Stream Data

Data fetching happens in parallel
4

Replace Content

Real content replaces fallback when ready

Error Handling

Create an error.js file to handle errors in Server Components:
app/meals/error.js
'use client';

export default function Error() {
  return (
    <main className="error">
      <h1>An error occurred!</h1>
      <p>Failed to fetch meal data. Please try again later.</p>
    </main>
  );
}
Error boundaries must be Client Components because they use React hooks internally (useEffect for error reporting).

Loading States

Create a loading.js file for automatic loading UI:
app/meals/loading.js
export default function Loading() {
  return (
    <div className="loading">
      <p>Loading meals...</p>
    </div>
  );
}
NextJS automatically wraps your page in a Suspense boundary using loading.js as the fallback.

Component Decision Tree

1

Does it need interactivity?

If YES → Client Component ('use client')If NO → Continue to step 2
2

Does it use React hooks?

If YES → Client Component ('use client')If NO → Continue to step 3
3

Does it access browser APIs?

If YES → Client Component ('use client')If NO → Continue to step 4
4

Default to Server Component

Keep it as a Server Component for better performance

Best Practices

Only add 'use client' to the smallest components that need it. This minimizes client JavaScript bundle size.
Fetch data in the component that needs it, not in a parent layout. This enables better streaming and parallel data fetching.
Start with Server Components and only add 'use client' when you need interactivity or browser APIs.
Pass Server Components as children to Client Components to preserve their server-only nature.

Performance Comparison

AspectClient ComponentsServer Components
Bundle SizeAdds to bundleZero bundle impact
Initial LoadSlower (more JS)Faster (less JS)
Data FetchingClient-side fetchServer-side fetch
InteractivityFull interactivityNone
Re-renderingOn state changesOnly on navigation
Backend AccessVia APIs onlyDirect access

Next Steps

Server Actions

Learn how to handle form submissions and mutations

Data Fetching Patterns

Explore advanced data fetching strategies

Build docs developers (and LLMs) love