Technology · Next.js
Next.js Components
Build Next.js components using server and client rendering strategies for performance.
TL;DR
- 01Server components render on server, no JavaScript sent.
- 02Client components (use client) render in browser with state.
- 03Combine both for optimal performance and interactivity.
Server Components
- 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} />; }
Client Components
- 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.
"use client"; import useSWR from "swr"; export function Profile({ id }) { const { data, error } = useSWR(`/api/users/${id}`, fetch); 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";
Tip: Default to server components for better performance — only use client components when you need interactivity.
Warning: "use client" at the file level makes the entire file client-side — keep client components minimal and separate.