Next.js

Next.js Server Components👨‍💻

Next.js is what makes React Server Components usable in production. React defined the spec, but the App Router provides the file-system conventions, the bundler integration, and the streaming infrastructure that turn RSC from a research project into something you ship. Every page.tsx and layout.tsx in the app/ directory is a Server Component by default. You don't opt in to server rendering — you opt out of it with 'use client' when a component needs interactivity.

This changes everything about how you build pages. You fetch data with await directly in the component body, not in getServerSideProps. You query your database from a page file, not from an API route. You import a 500KB syntax highlighter and it costs zero client-side JavaScript. The mental model is: your component runs on the server unless you explicitly tell it not to.

Key Takeaways

  • 1Every component in the `app/` directory is a Server Component by default — `'use client'` is the escape hatch, not the starting point. This is the opposite of the Pages Router mental model
  • 2Server Components can be `async` functions that `await` database queries, API calls, or file reads directly. No `useEffect`, no `getServerSideProps`, no intermediate API route
  • 3The `'use client'` directive marks a serialization boundary — everything below it (imports and children declared in that file) becomes part of the client bundle. Place it on leaf components, not on pages
  • 4Props crossing from Server to Client Components must be serializable: strings, numbers, booleans, arrays, plain objects, null. No functions, no Dates, no class instances, no Maps
  • 5Next.js streams Server Components via Suspense boundaries and `loading.js` files — the shell renders immediately while slow data resolves in parallel
  • 6Server Component code is never included in the client JavaScript bundle. A Prisma import, an `fs.readFile` call, or a secret API key used in a Server Component will never reach the browser

Master next.js server components

Take the Next.js Full-Stack course with hands-on lessons and challenges.

Examples

Async page component — query the database, skip the API route

tsx

This page queries Prisma with a join across three tables, calls notFound() for missing courses, and renders the full page — all on the server. The Prisma import, the database connection string, and the query logic never appear in the client bundle. The EnrollButton is a small Client Component for interactivity, and Reviews streams in independently via Suspense.

The serialization boundary — what can cross from server to client

tsx

The server fetches the full stats object (including Date instances and potentially sensitive fields), then picks only what each Client Component needs as serializable props. Dates become ISO strings. The auth check and redirect happen on the server before any rendering — no flash of unauthorized content.

Context providers without killing Server Components

tsx

The Providers wrapper is a Client Component because it uses hooks and React context. But its children prop is a slot — Next.js renders Sidebar and the page tree on the server first, then passes the serialized output through. Every page under this layout stays a Server Component. This is the standard pattern for apps that need React Query, theme providers, or auth context.

generateMetadata + generateStaticParams — SEO with Server Components

tsx

Server Components combine with generateStaticParams and generateMetadata to produce fully static, SEO-optimized pages at build time. The database queries run during the build, not at request time. Each page gets proper meta tags, Open Graph data, and pre-rendered HTML — all from the same component file. No getStaticProps, no separate data-fetching layer.

Streaming with loading.js and granular Suspense

tsx

loading.tsx gives an immediate skeleton when navigating to /dashboard. Within the page, each Suspense boundary wraps an async Server Component that fetches independently. ActiveCourses might resolve in 50ms while Leaderboard takes 400ms — they stream in as they complete. The user sees content progressively instead of waiting for the slowest query.

Wrapping third-party client libraries for use in Server Components

tsx

Many chart libraries (Recharts, Chart.js) use hooks internally but don't ship a 'use client' directive. Create a thin re-export file that adds the boundary. The page stays a Server Component — it runs the raw SQL query on the server, transforms the data into serializable props, and passes clean arrays to the client-side chart. The SQL and database connection stay on the server.

Common Mistakes

Mistake:

Putting `'use client'` on a page component because it contains one interactive element — this forces the entire page, all its imports, and all child components declared in that file onto the client

Fix:

Extract the interactive piece into its own file with `'use client'`. The page stays a Server Component and renders the client piece as a child. A course page with a bookmark button does not need to be a Client Component — only the button does.

Mistake:

Creating an API route in `app/api/` just to fetch data for a page in the same application — e.g. `fetch('/api/courses')` inside a Server Component

Fix:

Query the database or call the service directly in the Server Component. You are already on the server. API routes exist for external consumers or webhooks, not for your own pages. This eliminates a network round-trip and the serialization overhead of a REST layer.

Mistake:

Passing a `Date` object, a `Map`, or a callback function as props from a Server Component to a Client Component — these are not serializable and will throw at runtime

Fix:

Convert Dates to ISO strings (`.toISOString()`), Maps to plain objects (`Object.fromEntries()`), and move callback logic into Server Actions (`'use server'`) or define it inside the Client Component. Only primitives, plain objects, arrays, and null can cross the boundary.

Mistake:

Using `useEffect` and `useState` to fetch data in a component that could be a Server Component — adding `'use client'` just to do `useEffect(() => { fetch(...) }, [])`

Fix:

Remove `'use client'`, make the component `async`, and `await` the data directly. Server Components exist to eliminate this exact pattern. The component renders with the data already available — no loading state, no race conditions, no waterfall.

Best Practices

  • Default to Server Components for everything. Only add `'use client'` when you actually need useState, useEffect, event handlers, or browser APIs. If a component just renders data, it belongs on the server
  • Use `import 'server-only'` in files that contain database queries, API keys, or internal business logic. This causes a build error if the module is accidentally imported from a Client Component, catching the mistake before it reaches production
  • Wrap slow async Server Components in `<Suspense>` so they stream independently. A 400ms leaderboard query should not block a 50ms progress query from rendering — Suspense lets each section appear as its data arrives
  • Use `Promise.all()` for independent data fetches within the same Server Component. Sequential awaits add up: two 200ms queries take 400ms sequentially but only 200ms in parallel
  • Keep props crossing the boundary minimal and flat. Instead of passing an entire user object with 20 fields to a Client Component that only shows the name and avatar, pass `name: string` and `avatarUrl: string`. Fewer bytes cross the wire, and the contract is explicit
  • Combine `generateStaticParams` with async Server Components to build fully static SEO pages from database content at build time. The same component that renders the page also provides the metadata via `generateMetadata` — one file, complete SEO control

Summary

Next.js makes React Server Components practical through the App Router's file-system conventions. Every page.tsx and layout.tsx is a Server Component by default — you query databases, read files, and use heavy libraries without shipping any of that code to the browser. Add 'use client' only at the leaf level for interactive widgets. Data fetching happens directly in the component with async/await, replacing getServerSideProps and client-side useEffect patterns entirely. Props crossing from server to client must be serializable. Use Suspense boundaries to stream independent sections of a page in parallel. Use generateStaticParams and generateMetadata for build-time SEO pages. The discipline is simple: start on the server, push the client boundary as far down the tree as possible.

Practice Next.js with hands-on challenges

Learn next.js server components hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master Next.js with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.