Server and Client Components
Learn which components run on the server, which run in the browser, how 'use client' draws the line, and how to stream slow data with Suspense.
Think about how your Vite Reading List loads. The browser downloads the JavaScript, React renders an empty list, an effect starts a request, and only then do the books appear. Every component, even one that only displays text, ships to the browser as code.
Server Components change that. They run on the server, read the data they need while they render, and send the browser only the finished result. Components that need clicks or state still run in the browser, as Client Components. In the Next.js App Router, every component is a Server Component unless you say otherwise.
Components that run on the server
Here is the home page of the Next.js Reading List. Read it slowly, because nearly every line is something you could not do in the Vite version.
import { Suspense } from 'react';
import { getBooks } from '@/lib/books';
import { BookCard } from '@/components/BookCard';
export default function HomePage() {
return (
<>
<h1>My Reading List</h1>
<section aria-labelledby="books-heading">
<h2 id="books-heading">Your books</h2>
<Suspense fallback={<p>Loading your books...</p>}>
<BookList />
</Suspense>
</section>
</>
);
}
async function BookList() {
const books = await getBooks();
return (
<ul className="book-list">
{books.map((book) => (
<li key={book.id}>
<BookCard book={book} />
</li>
))}
</ul>
);
}BookList is an async function that awaits its data, then returns JSX. There is no useState for the books, no useEffect, no loading flag, and no API endpoint to call. The component runs on the server, where it can talk to the data source directly.
The data comes from lib/books.ts, which acts as a small pretend database:
import { connection } from 'next/server';
import type { Book } from './types';
// A pretend database: an array in the server's memory.
let books: Book[] = [
{ id: '1', title: 'The Hobbit', author: 'J.R.R. Tolkien', status: 'done', rating: 5 },
{ id: '2', title: 'Dune', author: 'Frank Herbert', status: 'reading' },
// ...three more books
];
export async function getBooks(): Promise<Book[]> {
await connection(); // read at request time, not at build time
return books;
}connection() tells Next.js to wait for a real visitor before running the rest of the function. Without it, Next.js would read this array once while building the app and keep that copy. You will see why in the Data and caching lesson. In a real app, the body of getBooks would be a database query instead.
Server Components give you three things:
- Data on the server. The component reads data while it renders, close to where the data lives.
- Less JavaScript in the browser. The code of a Server Component never ships to the browser, only its output. A Server Component can import a large library to format a date, and the browser downloads none of it.
- Secrets stay secret. Database passwords and API keys can be used in a Server Component, because the code never leaves the server.
What Server Components can't do
Because a Server Component has already run by the time the page reaches the browser, it can't respond to anything that happens in the browser:
| Server Component | Client Component | |
|---|---|---|
Read data with await while rendering | Yes | No (use use() or a data library) |
| Use secrets and server-only code | Yes | No |
useState, useReducer, useEffect | No | Yes |
Event handlers such as onClick | No | Yes |
Browser APIs such as localStorage | No | Yes |
| Its code is sent to the browser | No | Yes |
Which of these must be a Client Component?
Drawing the line with 'use client'
To make a component a Client Component, put the 'use client' directive at the very top of its file, above the imports. Here is the form that adds books. It needs state and events, so it is a Client Component:
'use client';
import { useState } from 'react';
export function AddBookForm() {
const [title, setTitle] = useState('');
// ...
}'use client' marks a boundary. That file, and everything it imports, becomes part of the JavaScript sent to the browser. Files above the boundary, like app/page.tsx, stay on the server. So you don't add 'use client' to every interactive file, only to the file where the server part hands over to the client part.
Client Components still render to HTML on the server for the first page load, so the user sees content straight away. Then React hydrates them in the browser, which means it attaches event handlers and state to that HTML.
Here is a book detail page split the right way. The page only displays text, so it stays on the server. The star rating is interactive, so it lives in its own Client Component file. In this sandbox everything runs in the browser and 'use client' does nothing, but the files are split exactly as they would be in Next.js.
Only StarRating and React itself would ship to the browser. The heading and paragraph arrive as finished HTML.
Passing data across the boundary
A Server Component can render a Client Component and pass it props, as BookPage passes initialRating to StarRating. Those props travel from the server to the browser over the network, so React has to turn them into data it can send. Values that can be sent like this are serializable:
- strings, numbers, booleans,
nullandundefined; - plain objects and arrays made of those;
Date,Map,Set, and Promises;- JSX elements, such as
children; - Server Functions, which you meet in the next lesson.
Ordinary functions, such as an onClick handler written in a Server Component, and instances of your own classes can't be sent.
// Wrong: a Server Component can't pass an ordinary function to the client
<StarRating onChange={(n) => console.log(n)} />
// Right: pass data, and let the Client Component own its handlers
<StarRating initialRating={book.rating} />Nesting server content inside client components
Client Components can't import Server Components, because anything a client file imports becomes client code too. They can still display server content, if it arrives as children. The Server Component renders the content, and the Client Component decides where to put it.
Collapsible never imports BookNotes. It receives it, already rendered, through children. So BookNotes stays a Server Component even though it appears inside a Client Component.
Deep diveContext providers in a Server Component tree
Server Components can't use context, because context lives in the browser's React tree. To give Client Components a shared value, write the provider as a Client Component that takes children:
'use client';
import { createContext } from 'react';
export const ThemeContext = createContext('light');
export function ThemeProvider({ children }: { children: React.ReactNode }) {
return <ThemeContext value="dark">{children}</ThemeContext>;
}Your root layout, a Server Component, can render <ThemeProvider>{children}</ThemeProvider>. The pages inside still render on the server, and any Client Component below can read the context. Wrap only the part of the tree that needs it.
A Server Component renders <SaveButton onSave={() => saveToDb()} />, where SaveButton is a Client Component. What happens?
Streaming slow parts with Suspense
Look again at app/page.tsx. The heading sits outside the <Suspense> boundary, and BookList sits inside it. You met Suspense on Day 6. On the server it does the same job: while BookList waits for getBooks(), Next.js sends the rest of the page with the fallback in place. When the data is ready, it streams the finished list into the same response, and React swaps it in.
So a slow query no longer blocks the whole page. The heading and the form appear immediately, and only the list shows "Loading your books...".
For a whole page, a loading.tsx file next to page.tsx does the same thing. Next.js wraps the page in a Suspense boundary and uses the file's component as the fallback. The detail route uses one:
export default function Loading() {
return <p>Loading book...</p>;
}With the default Cache Components setting, Next.js checks that data that is read on every request is inside a <Suspense> boundary (or a loading.tsx). If you forget, the dev overlay shows a "blocking route" message that names the component and suggests wrapping it in Suspense or caching it.
Shrink the client boundary
This detail page was written with 'use client' at the top so that one toggle could use state. That makes the whole page client code. Split it: move the toggle into NotesToggle.js with 'use client', and make App.js (the page) contain no hooks at all. The notes should arrive in NotesToggle as children.
Show solution
BookPage has no hooks and no directive, so in Next.js it is a Server Component again. It could be async and load the book and its notes from a database. Only NotesToggle and its single piece of state ship to the browser, and the notes travel in as children.
Day 7 progress