Books/React & Next.js Essentials/Data Fetching in Next.js

    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/await directly 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:

    NeedRequires Client?Example
    Displaying static dataNo (Server)Blog post content
    Fetching data on page loadNo (Server)User list page
    Button clicks / form inputYes (Client)Like button, search bar
    useState or useEffectYes (Client)Counter, form with validation
    Browser APIsYes (Client)localStorage, geolocation
    Third-party libraries that use hooksYes (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

    ConceptServer ComponentClient Component
    DirectiveNone (default)"use client"
    Data fetchingasync/await directlyuseEffect + useState
    InteractivityNoYes
    HooksNoYes
    Best forStatic content, data displayForms, 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.


    🌐 www.genai-mentor.ai