Server and Client Components
Fetch data on the server and add interactivity only where needed.
What you will learn
- Know the server/client split
- Fetch data in async components
- Use 'use client' correctly
In the App Router, components are Server Components by default. They run only on the server, ship no JavaScript to the browser, and can talk directly to databases and secret keys. When you need interactivity, you opt in to a Client Component with a directive.
Fetching data on the server
A server component can be async, so you fetch data right in the component with await. No useEffect, no loading state boilerplate, no API round trip from the browser.
// app/users/page.tsx (a server component)
interface User { id: number; name: string; }
export default async function Users() {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
const users: User[] = await res.json();
return (
<ul>
{users.map((u) => <li key={u.id}>{u.name}</li>)}
</ul>
);
}You can also query a database directly: const posts = await db.post.findMany(). That code and any secrets never reach the browser.
Client components
Add "use client" at the top of a file when you need state, effects, event handlers or browser APIs.
// app/components/LikeButton.tsx
"use client";
import { useState } from "react";
export default function LikeButton() {
const [likes, setLikes] = useState(0);
return <button onClick={() => setLikes(likes + 1)}>Like ({likes})</button>;
}Composing them
Server components can import and render client components, and pass data down as props. The rule of thumb: keep components on the server, and push the "use client" boundary down to the smallest interactive leaf. That keeps bundles small.
import LikeButton from "../components/LikeButton";
export default async function Post({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const post = await getPost(id); // runs on the server
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
<LikeButton /> {/* interactive island */}
</article>
);
}Caching and freshness
Next.js can cache fetched data and rendered pages. You control how fresh they are:
// always fresh
await fetch(url, { cache: "no-store" });
// refresh at most every 60 seconds
await fetch(url, { next: { revalidate: 60 } });
// page-level setting
export const revalidate = 3600;Pages that use no request-specific data can be generated at build time (static); pages using cookies, headers or no-store render per request (dynamic). Caching defaults have changed between versions, so check the docs for the version you use.
Using useState or onClick in a server component fails with a message telling you to add "use client". That is the signal to extract that interactive part into its own client component.
Try it yourself
Create a page that fetches five posts from jsonplaceholder.typicode.com/posts?_limit=5 on the server and renders them, with a separate client component that toggles showing each post body.
Show solution
// components/Expandable.tsx
"use client";
import { useState } from "react";
export default function Expandable({ title, body }: { title: string; body: string }) {
const [open, setOpen] = useState(false);
return (
<div>
<h3 onClick={() => setOpen(!open)} style={{ cursor: "pointer" }}>{title}</h3>
{open && <p>{body}</p>}
</div>
);
}
// app/posts/page.tsx (server)
export default async function Posts() {
const posts = await (await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5")).json();
return <>{posts.map((p: any) => <Expandable key={p.id} title={p.title} body={p.body} />)}</>;
}