Data Fetching in Next.js
Data Fetching in Next.js
One of the most important concepts in modern Next.js is the distinction between Server Components and Client Components. Understanding this will help you read AI-generated code and know where your data comes from.
Server Components vs Client Components
In the Next.js App Router, components are Server Components by default. This is a fundamental shift from traditional React.
Server Components (Default)
Server Components run on the server — they never run in the browser:
// app/users/page.tsx — this is a Server Component (no "use client" directive) interface User { id: number; name: string; email: string; } export default async function UsersPage() { // This fetch runs on the server — not in the browser! const response = await fetch("https://api.example.com/users"); const users: User[] = await response.json(); return ( <div> <h1>Users</h1> <ul> {users.map(user => ( <li key={user.id}> {user.name} — {user.email} </li> ))} </ul> </div> ); }
Notice: the component is async and uses await directly. No useEffect, no loading state management — it's much simpler.
Server Components can:
- Fetch data directly (with
async/await) - Access databases, file systems, and server-only resources
- Keep sensitive data (API keys, tokens) on the server
- Reduce the JavaScript sent to the browser
Server Components cannot:
- Use hooks (
useState,useEffect, etc.) - Use browser APIs (
window,document,localStorage) - Handle user interactions (clicks, form inputs)
Client Components
Client Components run in the browser and can use interactivity:
"use client"; // <-- This directive makes it a Client Component import { useState } from "react"; export default function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }
Client Components can:
- Use hooks (
useState,useEffect, etc.) - Handle user interactions (clicks, typing, forms)
- Use browser APIs (
window,localStorage)
Client Components cannot:
- Use
async/awaitdirectly in the component function - Access server-only resources directly
When to Use "use client"
Add "use client" at the top of a file when the component needs:
| Need | Requires Client? | Example |
|---|---|---|
| Displaying static data | No (Server) | Blog post content |
| Fetching data on page load | No (Server) | User list page |
| Button clicks / form input | Yes (Client) | Like button, search bar |
| useState or useEffect | Yes (Client) | Counter, form with validation |
| Browser APIs | Yes (Client) | localStorage, geolocation |
| Third-party libraries that use hooks | Yes (Client) | Chart libraries, modals |
The rule of thumb: Keep components as Server Components unless they need interactivity. Only add "use client" when you must.
What to ask your AI: "Does this component need to be a Client Component? It does [describe what it does]."
Fetching Data in Server Components
Server Components make data fetching incredibly clean:
Simple Fetch
// app/products/page.tsx interface Product { id: string; name: string; price: number; description: string; } export default async function ProductsPage() { const products: Product[] = await fetch("https://api.example.com/products", { cache: "no-store", // Always fetch fresh data }).then(res => res.json()); return ( <div className="grid grid-cols-3 gap-4"> {products.map(product => ( <div key={product.id} className="border rounded p-4"> <h2 className="font-bold">{product.name}</h2> <p className="text-gray-600">{product.description}</p> <p className="text-lg font-bold mt-2">${product.price}</p> </div> ))} </div> ); }
Fetching from a Database
// app/posts/page.tsx import { db } from "@/lib/database"; export default async function PostsPage() { // Query the database directly — this runs on the server const posts = await db.query("SELECT * FROM posts ORDER BY created_at DESC"); return ( <div> {posts.map(post => ( <article key={post.id}> <h2>{post.title}</h2> <p>{post.excerpt}</p> </article> ))} </div> ); }
Parallel Data Fetching
When you need multiple pieces of data, fetch them in parallel:
// app/dashboard/page.tsx export default async function DashboardPage() { // Fetch in parallel — much faster than sequential const [users, products, orders] = await Promise.all([ fetch("https://api.example.com/users").then(res => res.json()), fetch("https://api.example.com/products").then(res => res.json()), fetch("https://api.example.com/orders").then(res => res.json()), ]); return ( <div> <h1>Dashboard</h1> <StatCard title="Users" count={users.length} /> <StatCard title="Products" count={products.length} /> <StatCard title="Orders" count={orders.length} /> </div> ); }
What to ask your AI: "Fetch [this data] in a Server Component. Use parallel fetching if there are multiple data sources."
Client-Side Fetching with useEffect
Sometimes you need to fetch data on the client — for search results, infinite scrolling, or data that depends on user interaction:
"use client"; import { useState, useEffect } from "react"; interface SearchResult { id: string; title: string; description: string; } export default function SearchPage() { const [query, setQuery] = useState(""); const [results, setResults] = useState<SearchResult[]>([]); const [loading, setLoading] = useState(false); useEffect(() => { if (!query.trim()) { setResults([]); return; } const fetchResults = async () => { setLoading(true); try { const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`); const data = await response.json(); setResults(data); } catch (error) { console.error("Search failed:", error); } finally { setLoading(false); } }; const debounceTimer = setTimeout(fetchResults, 300); return () => clearTimeout(debounceTimer); }, [query]); return ( <div> <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search..." className="border rounded px-4 py-2 w-full" /> {loading && <p>Searching...</p>} <ul> {results.map(result => ( <li key={result.id}> <h3>{result.title}</h3> <p>{result.description}</p> </li> ))} </ul> </div> ); }
Loading and Error States
Server Component Loading (loading.tsx)
// app/products/loading.tsx export default function ProductsLoading() { return ( <div className="grid grid-cols-3 gap-4"> {Array.from({ length: 6 }).map((_, i) => ( <div key={i} className="border rounded p-4 animate-pulse"> <div className="h-6 bg-gray-200 rounded mb-2"></div> <div className="h-4 bg-gray-200 rounded w-2/3"></div> </div> ))} </div> ); }
Client Component Loading Pattern
"use client"; function DataComponent() { const [data, setData] = useState<Data | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { fetchData() .then(setData) .catch(err => setError(err.message)) .finally(() => setLoading(false)); }, []); if (loading) return <LoadingSkeleton />; if (error) return <ErrorMessage message={error} />; if (!data) return <EmptyState />; return <DataDisplay data={data} />; }
Mixing Server and Client Components
The real power comes from combining both. Keep data fetching on the server, interactivity on the client:
// app/products/page.tsx — Server Component (fetches data) import { ProductList } from "./ProductList"; export default async function ProductsPage() { const products = await fetch("https://api.example.com/products").then( res => res.json() ); return ( <div> <h1>Products</h1> {/* Pass server-fetched data to a client component */} <ProductList initialProducts={products} /> </div> ); }
// app/products/ProductList.tsx — Client Component (handles interactivity) "use client"; import { useState } from "react"; interface Product { id: string; name: string; price: number; category: string; } export function ProductList({ initialProducts }: { initialProducts: Product[] }) { const [filter, setFilter] = useState(""); const filtered = initialProducts.filter(p => p.name.toLowerCase().includes(filter.toLowerCase()) ); return ( <div> <input value={filter} onChange={(e) => setFilter(e.target.value)} placeholder="Filter products..." /> <ul> {filtered.map(product => ( <li key={product.id}>{product.name} — ${product.price}</li> ))} </ul> </div> ); }
This pattern is extremely common: fetch on the server, interact on the client.
What to ask your AI: "Build a page that fetches [data] on the server and passes it to a client component with [search/filter/sort] functionality."
Key Takeaways
| Concept | Server Component | Client Component |
|---|---|---|
| Directive | None (default) | "use client" |
| Data fetching | async/await directly | useEffect + useState |
| Interactivity | No | Yes |
| Hooks | No | Yes |
| Best for | Static content, data display | Forms, buttons, dynamic UI |
What's Next?
Now that you can fetch and display data, the next tutorial covers Styling with Tailwind CSS — how to make your React and Next.js apps look great.