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.
Master next.js server components
Take the Next.js Full-Stack course with hands-on lessons and challenges.
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 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.
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.
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.
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.
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.
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
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.
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
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.
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
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.
Using `useEffect` and `useState` to fetch data in a component that could be a Server Component — adding `'use client'` just to do `useEffect(() => { fetch(...) }, [])`
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.
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.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.