Next.js Components
Build Next.js components using server and client rendering strategies for performance.
TL;DR
- Server components render on server, no JavaScript sent.
- Client components (use client) render in browser with state.
- Combine both for optimal performance and interactivity.
Server Component Patterns
- Server components are the default in App Router.
// app/components/BlogPost.tsx export default async function BlogPost({ slug }) { const post = await fetchPost(slug); return <article>{post.content}</article>; } - Can access databases, secrets, and APIs directly.
- No JavaScript sent to browser for these components.
- Cannot use hooks or browser APIs.
- Use async/await directly in the component to fetch data.
export default async function UserProfile({ id }) { const user = await db.user.findUnique({ where: { id } }); return <p>{user.name}</p>; } - Pass props from server components to client components safely.
export default async function Page() { const settings = await getSettings(); return <ThemeProvider theme={settings.theme} />; }
Interactive Patterns
- Mark components to render on client with "use client".
"use client"; import { useState } from "react"; export default function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(count + 1)}>{count}</button>; } - Can use React hooks and browser APIs.
- Must declare at top of file.
- Use for interactive features.
- Use useEffect to run code only after the component mounts.
"use client"; import { useEffect, useState } from "react"; export function Clock() { const [time, setTime] = useState(""); useEffect(() => { setTime(new Date().toLocaleTimeString()); }, []); return <p>{time}</p>; } - Access browser APIs like localStorage inside client components.
"use client"; export function SaveButton({ data }) { function save() { localStorage.setItem("draft", JSON.stringify(data)); } return <button onClick={save}>Save Draft</button>; }
Server and Client Composition
- Pass server components as children to client components.
"use client"; export default function Layout({ children }) { return <div>{children}</div>; } // children can be server components <Layout> <ServerComponent /> </Layout> - Keep sensitive data in server components, pass only safe props.
// Server component fetches and sanitizes data export default async function Page() { const post = await getPost(); // has private fields return <PostCard title={post.title} body={post.body} />; } - Use context providers at the root for shared client state.
"use client"; export function ThemeProvider({ children }) { const [theme, setTheme] = useState("light"); return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> ); } - Wrap only interactive parts in client components to minimize bundle.
// app/page.tsx (server) import LikeButton from "./LikeButton"; // only this is client export default async function Post() { const post = await getPost(); return ( <article> <h1>{post.title}</h1> <LikeButton postId={post.id} /> </article> ); } - Avoid importing server-only modules in client components.
import "server-only"; // throws if imported by a client bundle
Data Fetching Patterns
- Fetch in server components for better performance.
export default async function Products() { const res = await fetch('/api/products'); const products = await res.json(); return ( <ul> {products.map(p => <li key={p.id}>{p.name}</li>)} </ul> ); } - Fetch from client for real-time or user-specific data.
"use client"; import { useEffect, useState } from "react"; export function LiveFeed() { const [data, setData] = useState(null); useEffect(() => { fetch('/api/live-data') .then(r => r.json()) .then(setData); }, []); return <div>{data?.message}</div>; } - Use SWR for client-side fetching with caching and revalidation — provide a fetcher that parses JSON.
"use client"; import useSWR from "swr"; const fetcher = (url: string) => fetch(url).then(r => r.json()); export function Profile({ id }) { const { data, error } = useSWR(`/api/users/${id}`, fetcher); if (error) return <p>Error</p>; return <p>{data?.name}</p>; } - Parallel fetch multiple server requests to reduce wait time.
export default async function Page() { const [user, posts] = await Promise.all([getUser(), getPosts()]); return <div><UserCard user={user} /><PostList posts={posts} /></div>; } - Deduplicate fetch calls with the built-in request memoization.
// Both components call getUser() — fetched only once per request async function Header() { const u = await getUser(); return <p>{u.name}</p>; } async function Sidebar() { const u = await getUser(); return <p>{u.role}</p>; }
Component Organization
- Separate server and client concerns into different folders.
app/ components/ server/ BlogPost.tsx # Server component Header.tsx # Server component client/ Counter.tsx # Client component Modal.tsx # Client component - Keep client components small and focused.
"use client"; // Only interactive part is client export function Favorite({ postId }) { const [liked, setLiked] = useState(false); return <button onClick={() => setLiked(!liked)}>{liked ? "Liked" : "Like"}</button>; } - Colocate components near the pages that use them.
app/ blog/ page.tsx components/ PostCard.tsx PostList.tsx - Export reusable components from a shared ui folder.
// app/ui/Button.tsx export function Button({ children, onClick }) { return <button className="btn" onClick={onClick}>{children}</button>; } - Use index files to simplify imports from component folders.
// app/ui/index.ts export { Button } from "./Button"; export { Card } from "./Card"; // import { Button, Card } from "@/app/ui";
Tips
- Default to server components for better performance — only use client components when you need interactivity.
Warnings
- "use client" at the file level makes the entire file client-side — keep client components minimal and separate.
FAQ
Use Client Components when you need browser APIs, event listeners, useState, or useEffect. If your component is purely for rendering data without interactivity, keep it as a Server Component to avoid shipping unnecessary JavaScript to the browser.
Yes — you can import and render Client Components inside Server Components. The boundary only goes one way: Client Components cannot import Server Components, but you can pass Server Components as children or props to Client Components.
The 'use client' directive marks a module boundary — everything imported by that file is also bundled for the client. Extract interactive logic into a small, focused component and add 'use client' only there to keep the rest of the tree server-rendered.
Directly call async functions or await fetch() inside the component body — Server Components are async by default. There's no need for useEffect or SWR; the data is fetched at render time on the server and the result is streamed to the client.
A layout wraps multiple pages and persists across navigation, while a page is the unique UI for a route segment. Both default to Server Components; only add 'use client' if the layout or page itself requires interactivity, otherwise keep data fetching server-side for better performance.