Skip to main content
Next.js Fundamentals·Lesson 4 of 5

Data Fetching and Server Components

Server Components vs Client Components

In Next.js, every component is a Server Component by default. This means it runs on the server, can access databases and APIs directly, and sends only the rendered HTML to the browser.

Client Components run in the browser and are needed when you use interactivity — state, effects, event handlers, or browser APIs.

// This is a Server Component (default)
// It runs on the server — no JavaScript sent to the browser
export default async function ProductList() {
  const products = await fetch('https://api.example.com/products')
    .then(res => res.json())

  return (
    <ul>
      {products.map(p => (
        <li key={p.id}>{p.name} — ${p.price}</li>
      ))}
    </ul>
  )
}

To make a component a Client Component, add the "use client" directive at the top:

'use client'

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)

  return (
    <button onClick={()=> setCount(count + 1)}>
      Clicks: {count}
    </button>
  )
}

When to Use Each

Use Server Components when...Use Client Components when...
Fetching dataHandling user events (click, input)
Accessing backend resourcesUsing React state or effects
Keeping sensitive data on the serverUsing browser-only APIs
Reducing client JavaScriptAdding interactivity

A good rule: keep components on the server by default and only add "use client" when you need interactivity.

Fetching Data in Server Components

Server Components can use async/await directly — no useEffect or useState needed:

// app/users/page.tsx
interface User {
  id: number
  name: string
  email: string
}

export default async function UsersPage() {
  const res = await fetch('https://jsonplaceholder.typicode.com/users')
  const users: User[] = await res.json()

  return (
    <div>
      <h1>Users</h1>
      {users.map(user => (
        <div key={user.id}>
          <h2>{user.name}</h2>
          <p>{user.email}</p>
        </div>
      ))}
    </div>
  )
}

This is simpler than the traditional React approach — no loading states to manage, no useEffect timing issues.

Caching and Revalidation

Next.js extends fetch with caching options:

// Cache the response indefinitely (default behavior)
const res = await fetch('https://api.example.com/data')

// Revalidate every 60 seconds
const res = await fetch('https://api.example.com/data', {
  next: { revalidate: 60 }
})

// Never cache — always fetch fresh data
const res = await fetch('https://api.example.com/data', {
  cache: 'no-store'
})
  • Static (default) — data is fetched at build time and cached
  • Revalidate — data is cached but refreshed after a time interval
  • Dynamic — data is fetched on every request

Loading and Error States

Next.js has special files for handling loading and errors at the route level:

// app/users/loading.tsx
export default function Loading() {
  return <p>Loading users...</p>
}
// app/users/error.tsx
'use client'

export default function Error({ error, reset }) {
  return (
    <div>
      <p>Something went wrong: {error.message}</p>
      <button onClick={()=> reset()}>Try Again</button>
    </div>
  )
}

The loading.tsx file shows automatically while the page's data is being fetched. The error.tsx file catches errors and gives users a way to recover.

Composing Server and Client Components

A common pattern is wrapping interactive Client Components inside data-fetching Server Components:

// app/dashboard/page.tsx (Server Component)
import Chart from './chart'

export default async function Dashboard() {
  const data = await fetch('https://api.example.com/stats')
    .then(res => res.json())

  // Pass server-fetched data to an interactive client component
  return <Chart data={data} />
}
// app/dashboard/chart.tsx (Client Component)
'use client'

export default function Chart({ data }) {
  // Interactive chart using browser APIs
  return <canvas id="chart">{/* render chart with data */}</canvas>
}

This keeps your data fetching on the server while letting the chart handle mouse events and animations in the browser.

Practical Exercise

Try building a simple page that fetches data:

  1. Create app/posts/page.tsx as a Server Component
  2. Fetch posts from https://jsonplaceholder.typicode.com/posts?_limit=10
  3. Render each post's title and body
  4. Add a loading.tsx file in the same folder
  5. Test it by visiting /posts