Data fetching and server components

Fetch on the server, choose your caching deliberately, keep secrets out of the client bundle, and mutate data with server actions.

Server by default, client on request

Components in the App Router render on the server unless a file opts in with the 'use client' directive at the top. Server components can be async, read from a database directly, and never ship their code to the browser.

// app/guides/page.js — async server component, no loading spinner needed
import { db } from '@/lib/db'

export default async function Guides() {
  const guides = await db.guide.findMany({ orderBy: { title: 'asc' } })
  return <GuideList guides={guides} />
}
'use client'   // only what needs interactivity

import { useState } from 'react'

export default function SearchBox({ initial = '' }) {
  const [q, setQ] = useState(initial)
  return <input value={q} onChange={e => setQ(e.target.value)} />
}
NeedComponent type
Read the database or a private APIServer
Use an API key or secretServer
onClick, useState, useEffect, browser APIsClient
Render mostly static contentServer
Wrap third-party browser-only widgetsClient
Send data from server to clientProps, and they must be serialisable

Choosing cache behaviour

// fresh on every request
const live = await fetch(url, { cache: 'no-store' })

// cached, revalidated in the background at most every 60 seconds
const feed = await fetch(url, { next: { revalidate: 60 } })

// tagged so a mutation can invalidate exactly this data
const docs = await fetch(url, { next: { tags: ['docs'] } })

// after a write
import { revalidateTag, revalidatePath } from 'next/cache'
revalidateTag('docs')
revalidatePath('/guides')
  • In current releases, fetch is no longer cached by default — opt in per request. Caching is a decision you make, not a default you inherit.
  • A page becomes dynamic as soon as it reads a request value such as cookies, headers or a search parameter, unless you handle that explicitly.
  • Tag-based invalidation scales better than paths: tag the data, then invalidate the tag wherever the mutation happens.
  • Never put a secret in a component that renders on the client — environment variables without the NEXT_PUBLIC_ prefix are simply undefined in the browser bundle.
⚠️
A client component is not a private environment. Anything you import into it — API keys, internal URLs, raw database rows — is shipped to every visitor. Keep the secret on the server and pass down only the fields the UI renders.

Mutations with server actions

// app/guides/actions.js
'use server'

import { revalidatePath } from 'next/cache'
import { db } from '@/lib/db'

export async function createGuide(formData) {
  const title = String(formData.get('title') || '').trim()
  if (!title) return { error: 'Title is required' }

  const session = await getSession()
  if (!session) return { error: 'Not signed in' }

  await db.guide.create({ data: { title, ownerId: session.userId } })
  revalidatePath('/guides')
  return { ok: true }
}
// app/guides/new/page.js
import { createGuide } from '../actions'

export default function NewGuide() {
  return (
    <form action={createGuide}>
      <input name="title" required />
      <button type="submit">Create</button>
    </form>
  )
}
  • A server action is a function marked with 'use server' that the client can invoke; Next.js generates the endpoint and the fetch call.
  • It runs on the server, so it must re-check authentication and validation even if the form already did — a client check is a convenience, not a boundary.
  • Return plain serialisable values for error feedback rather than throwing, so the UI can render a message.
  • Wrap slow sections in a Suspense boundary so fast parts of the page are not held back by the slowest query.
  • Load independent data with parallel awaits (Promise.all) instead of chaining awaits, which create a request waterfall.

FAQ

Can a server component pass a function to a client component?
Only a server action. Ordinary functions are not serialisable, so pass data as props and let the client component define its own handlers.
Why is my page always dynamic even though nothing changes?
Something in the render path reads a request value — cookies, headers, a search parameter, or an uncached fetch. Find that call; that is what forces per-request rendering.

File-based routing and layouts Building and deploying

Last refreshed 2026-09-18.