---
title: "Server and Client Components"
description: "Learn when components run on the server vs client, how environment variables differ between them, and how to compose Server Components inside Client wrappers."
canonical_url: "https://vercel.com/academy/nextjs-foundations/server-and-client-components"
md_url: "https://vercel.com/academy/nextjs-foundations/server-and-client-components.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-04-11T11:12:57.446Z"
content_type: "lesson"
course: "nextjs-foundations"
course_title: "Next.js Foundations"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Server and Client Components

\*\*Note: This Course Is in Beta\*\*

You're getting early access to this course as it's being refined. Have feedback? Please share it in the widget at the bottom of each lesson.

# Server and Client Components

You added a `useState` to track a counter. The page crashes. "useState is not defined." You didn't change anything else. What happened?

In the App Router, **all components are Server Components by default**. Server Components run on the server where there's no `useState`, no `onClick`, no browser. When you need interactivity, you opt into Client Components with the `'use client'` directive.

This lesson shows you exactly when each type applies and how to compose them together.

## Outcome

A working demo showing environment variable access differences between Server and Client Components, plus an interactive counter that demonstrates the `'use client'` boundary.

## Fast Track

1. Visit `http://localhost:3000/env-demo` to see the existing Server/Client component split
2. Add `useState` to the Client Component to make it interactive
3. Verify that server-only env vars show `undefined` on the client

## The Decision Model

```
┌─────────────────────────────────────────────────────────────┐
│                    Need This?                               │
├─────────────────────────────────────────────────────────────┤
│  useState, useEffect, useContext          → Client          │
│  onClick, onChange, onSubmit              → Client          │
│  Browser APIs (localStorage, window)      → Client          │
│  Third-party libs needing browser         → Client          │
├─────────────────────────────────────────────────────────────┤
│  Direct database/file access              → Server          │
│  Secret env vars (API keys, tokens)       → Server          │
│  Heavy dependencies (keep off client bundle) → Server       │
│  SEO-critical content                     → Server          │
│  Everything else                          → Server (default)│
└─────────────────────────────────────────────────────────────┘
```

**Rule of thumb:** Start with Server Components. Add `'use client'` only when you need interactivity or browser APIs.

## How It Works

When you add `'use client'` at the top of a file, you're declaring a **boundary**. Everything in that file and everything it imports becomes part of the client bundle (the JavaScript files sent to and executed in the browser). This is why you want to keep client boundaries as small as possible.

```tsx title="apps/web/src/components/counter.tsx"
'use client'

import { useState } from 'react'

export function Counter() {
  const [count, setCount] = useState(0)
  
  return (
    <button
      type="button"
      onClick={() => setCount(count + 1)}
      className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
    >
      Count: {count}
    </button>
  )
}
```

Without `'use client'`, this component would error because `useState` doesn't exist on the server.

\*\*Note: The Boundary, Not the Component\*\*

`'use client'` marks the **entry point** into client code. You don't need it in every file that uses hooks, only in files that are directly imported by Server Components. Child components of a Client Component are automatically client-side.

## Environment Variables: The Security Boundary

Your environment variables were set up in [Project Setup](/01-foundation-and-setup/project-setup) via `vercel link`. The starter already has two components that demonstrate the Server/Client split:

```tsx title="apps/web/src/components/server-env-display.tsx"
export function ServerEnvDisplay() {
  return (
    <div className="rounded border p-4">
      <h3 className="font-bold">Server Component</h3>
      <p>Public: {process.env.NEXT_PUBLIC_APP_NAME}</p>
      <p>Server-only: {process.env.INTERNAL_CONFIG}</p>
    </div>
  )
}
```

```tsx title="apps/web/src/components/client-env-display.tsx"
'use client'

export function ClientEnvDisplay() {
  return (
    <div className="rounded border p-4">
      <h3 className="font-bold">Client Component</h3>
      <p>Public: {process.env.NEXT_PUBLIC_APP_NAME}</p>
      <p>Server-only: {process.env.INTERNAL_CONFIG || 'undefined'}</p>
    </div>
  )
}
```

**Key difference:**

- `NEXT_PUBLIC_*` variables are inlined (embedded directly as static values) into the client bundle at build time, so both components can access them
- Non-prefixed variables (`INTERNAL_CONFIG`) exist only in the Node.js environment, invisible to the browser

\*\*Warning: Security Implication\*\*

Never put secrets (API keys, database URLs, tokens) in `NEXT_PUBLIC_*` variables. They're visible in browser dev tools and your JavaScript bundle.

## Step 1: Add Interactivity to the Client Component

Update the Client Component to include a counter, demonstrating why `'use client'` is necessary:

```tsx title="apps/web/src/components/client-env-display.tsx" {1,3,6-8,14-19}
'use client'

import { useState } from 'react'

export function ClientEnvDisplay() {
  const [clicks, setClicks] = useState(0)
  
  const handleClick = () => setClicks(clicks + 1)
  
  return (
    <div className="rounded border p-4">
      <h3 className="font-bold">Client Component</h3>
      <p>Public: {process.env.NEXT_PUBLIC_APP_NAME}</p>
      <p>Server-only: {process.env.INTERNAL_CONFIG || 'undefined'}</p>
      <button
        type="button"
        onClick={handleClick}
        className="mt-2 rounded bg-blue-500 px-3 py-1 text-white hover:bg-blue-600"
      >
        Clicked {clicks} times
      </button>
    </div>
  )
}
```

## Step 2: Test the Difference

Navigate to `http://localhost:3000/env-demo`. You should see:

```
Environment Variable Demo

┌──────────────────────────────────────┐
│ Server Component                     │
│ Public: ACME Corporation             │
│ Server-only: server-only-value       │
└──────────────────────────────────────┘

┌──────────────────────────────────────┐
│ Client Component                     │
│ Public: ACME Corporation             │
│ Server-only: undefined               │
│ [Clicked 0 times]                    │
└──────────────────────────────────────┘
```

Click the button. The count increases. This interactivity is only possible because of `'use client'`.

Now open browser DevTools (F12) → Network tab → refresh the page. Look at the HTML response. The Server Component's `INTERNAL_CONFIG` value appears in the initial HTML (rendered on the server). The Client Component's value shows `undefined` because that code runs in the browser.

## Composition Pattern: Server Inside Client

What if you need a Client Component wrapper (for interactivity) but want to keep some content server-rendered?

**Pass Server Components as children.** The Server Component renders on the server, then gets passed to the Client Component as already-rendered content.

Create a collapsible wrapper that needs client-side state:

```tsx title="apps/web/src/components/collapsible.tsx"
'use client'

import { useState, type ReactNode } from 'react'

export function Collapsible({ 
  title, 
  children 
}: { 
  title: string
  children: ReactNode 
}) {
  const [isOpen, setIsOpen] = useState(true)
  
  return (
    <div className="rounded border">
      <button
        type="button"
        onClick={() => setIsOpen(!isOpen)}
        className="flex w-full items-center justify-between p-4 text-left font-semibold hover:bg-gray-50"
      >
        {title}
        <span>{isOpen ? '−' : '+'}</span>
      </button>
      {isOpen && <div className="border-t p-4">{children}</div>}
    </div>
  )
}
```

Now use it in the env-demo page, passing the Server Component as children:

```tsx title="apps/web/src/app/env-demo/page.tsx" {2,9-11}
import { ServerEnvDisplay } from '@/components/server-env-display'
import { Collapsible } from '@/components/collapsible'
import { ClientEnvDisplay } from '@/components/client-env-display'

export default function EnvDemoPage() {
  return (
    <main className="flex flex-col gap-4 p-4">
      <h1 className="text-2xl font-bold">Environment Variable Demo</h1>
      <Collapsible title="Server-Rendered Content">
        <ServerEnvDisplay />
      </Collapsible>
      <ClientEnvDisplay />
    </main>
  )
}
```

The `Collapsible` wrapper is a Client Component (needs `useState` for toggle), but `ServerEnvDisplay` inside it is still server-rendered. The children are rendered on the server first, then passed as props.

\*\*Note: Why This Matters\*\*

This pattern keeps your client bundle small. The `ServerEnvDisplay` code never ships to the browser. Only the rendered HTML output does. For components with heavy dependencies or sensitive logic, this is significant.

## Try It

1. **Environment variable visibility:**
   - Server Component shows both `NEXT_PUBLIC_APP_NAME` and `INTERNAL_CONFIG` values
   - Client Component shows `NEXT_PUBLIC_APP_NAME` but `INTERNAL_CONFIG` is `undefined`

2. **Interactivity works:**
   - Click the "Clicked X times" button
   - Counter increments on each click

3. **Composition works:**
   - Click the "Server-Rendered Content" header to collapse/expand
   - The Server Component content toggles visibility
   - Check View Source (Ctrl+U): `server-only-value` appears in the initial HTML

## Commit

```bash
git add -A
git commit -m "feat: demonstrate server/client component boundaries"
git push
```

## Done-When

- [ ] `http://localhost:3000/env-demo` shows both Server and Client components
- [ ] Server Component displays `INTERNAL_CONFIG` value; Client Component shows `undefined`
- [ ] Clicking the button increments the counter (proves `useState` works)
- [ ] Collapsible wrapper toggles Server Component visibility
- [ ] View Source (Ctrl+U) shows `server-only-value` in initial HTML

## Troubleshooting

useState is not defined

```
Error: useState is not a function
```

You're using a hook in a Server Component. Add `'use client'` at the top of the file:

```tsx
'use client'

import { useState } from 'react'
```

Environment variables show undefined

1. Run `vercel env pull` from `apps/web/` to pull env vars from your Vercel project
2. Make sure `.env.local` is in `apps/web/`, not the repo root
3. Restart the dev server after pulling env vars
4. `NEXT_PUBLIC_*` vars are inlined at build time, so changes require a restart

Hydration mismatch error

```
Error: Hydration failed because the server rendered content doesn't match the client
```

This happens when server and client render different content. Common causes:

- Using `Date.now()` or `Math.random()` without proper handling
- Browser-only values like `window.innerWidth` without checking

For env vars, this shouldn't happen if you're only reading `process.env` values.

\*\*Note: Still Stuck?\*\*

Ask your coding agent for help. Paste the error message and it can diagnose the issue.

````markdown title="Prompt: Debug Server Component useState Error"
I'm getting a React hooks error in my Next.js 16 app.

**Error:**
```
___PASTE_EXACT_ERROR_MESSAGE___
```

Example errors:
- "useState is not a function"
- "React hooks can only be called inside a function component"
- "Invalid hook call"

**My component:**
```tsx
// File: _____
// Example: src/app/dashboard/counter.tsx

___PASTE_YOUR_FULL_COMPONENT___
```

**Questions:**
1. Is this a Server Component or Client Component?
2. Am I missing a `'use client'` directive?
3. Am I importing from the wrong place?

Help me understand whether this is a Server/Client boundary issue and how to fix it.
````

````markdown title="Prompt: Debug Hydration Mismatch"
I'm getting a hydration mismatch error in Next.js.

**Error:**
```
___PASTE_HYDRATION_ERROR___
```

Example: "Hydration failed because the server rendered content doesn't match the client"

**My component:**
```tsx
// File: _____

___PASTE_YOUR_COMPONENT___
```

**What I think might differ between server and client:**
- [ ] I'm using `Date.now()` or `Math.random()`
- [ ] I'm checking `window` or `document`
- [ ] I'm using browser-only APIs
- [ ] I'm not sure

**The mismatch I see:**
- Server renders: _____
- Client renders: _____

Help me identify what's causing the server/client mismatch and how to fix it.
````

## Solution

Complete file implementations

### Server Component (unchanged from starter)

```tsx title="apps/web/src/components/server-env-display.tsx"
export function ServerEnvDisplay() {
  return (
    <div className="rounded border p-4">
      <h3 className="font-bold">Server Component</h3>
      <p>Public: {process.env.NEXT_PUBLIC_APP_NAME}</p>
      <p>Server-only: {process.env.INTERNAL_CONFIG}</p>
    </div>
  )
}
```

### Client Component with Interactivity

```tsx title="apps/web/src/components/client-env-display.tsx"
'use client'

import { useState } from 'react'

export function ClientEnvDisplay() {
  const [clicks, setClicks] = useState(0)
  
  return (
    <div className="rounded border p-4">
      <h3 className="font-bold">Client Component</h3>
      <p>Public: {process.env.NEXT_PUBLIC_APP_NAME}</p>
      <p>Server-only: {process.env.INTERNAL_CONFIG || 'undefined'}</p>
      <button
        type="button"
        onClick={() => setClicks(clicks + 1)}
        className="mt-2 rounded bg-blue-500 px-3 py-1 text-white hover:bg-blue-600"
      >
        Clicked {clicks} times
      </button>
    </div>
  )
}
```

### Collapsible Wrapper (Composition Pattern)

```tsx title="apps/web/src/components/collapsible.tsx"
'use client'

import { useState, type ReactNode } from 'react'

export function Collapsible({ 
  title, 
  children 
}: { 
  title: string
  children: ReactNode 
}) {
  const [isOpen, setIsOpen] = useState(true)
  
  return (
    <div className="rounded border">
      <button
        type="button"
        onClick={() => setIsOpen(!isOpen)}
        className="flex w-full items-center justify-between p-4 text-left font-semibold hover:bg-gray-50"
      >
        {title}
        <span>{isOpen ? '−' : '+'}</span>
      </button>
      {isOpen && <div className="border-t p-4">{children}</div>}
    </div>
  )
}
```

### Page Composing Both

```tsx title="apps/web/src/app/env-demo/page.tsx"
import { ServerEnvDisplay } from '@/components/server-env-display'
import { Collapsible } from '@/components/collapsible'
import { ClientEnvDisplay } from '@/components/client-env-display'

export default function EnvDemoPage() {
  return (
    <main className="flex flex-col gap-4 p-4">
      <h1 className="text-2xl font-bold">Environment Variable Demo</h1>
      <Collapsible title="Server-Rendered Content">
        <ServerEnvDisplay />
      </Collapsible>
      <ClientEnvDisplay />
    </main>
  )
}
```

\*\*Side Quest: Build Theme Toggle with Composition Pattern\*\*

## Learn More

- [Server and Client Components](https://nextjs.org/docs/app/getting-started/server-and-client-components) - Official guide on the component model
- [use client directive](https://nextjs.org/docs/app/api-reference/directives/use-client) - API reference for the directive
- [Environment Variables](https://nextjs.org/docs/app/building-your-application/configuring/environment-variables) - How to configure and expose env vars

## What's Next

You now understand the Server/Client boundary and how to compose components across it. In the next lesson, you'll learn how to handle **dynamic routes** with params, like `/blog/[slug]`, and how to access those params in both Server and Client Components.


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
