Next.js Lesson 11 – Data Fetching Basics | Dataplexa
LESSON 11

Data Fetching Basics

Master client-side and server-side data fetching patterns to power NewsWave with dynamic content from APIs and external sources.

Data fetching represents the bridge between your static Next.js application and the dynamic world of APIs, databases, and external services. Unlike traditional React applications that rely entirely on client-side requests, Next.js opens up multiple pathways for bringing data into your components. Think of it like a restaurant with different service models — you can have food delivered to your table (server-side), order at the counter (client-side), or even have meals pre-prepared before you arrive (static generation). The beauty of Next.js data fetching lies in its flexibility and performance optimizations. Where React applications typically show loading spinners while fetching data after the page loads, Next.js can fetch data before the user even sees the page. This approach eliminates the dreaded flash of loading content and provides instant user experiences that feel native and responsive. Data fetching patterns directly impact your application's performance, SEO capabilities, and user experience. A news site like NewsWave benefits enormously from pre-fetched article content that appears instantly, while still maintaining the ability to fetch fresh comments or related articles dynamically. Understanding these patterns helps you choose the right tool for each scenario.

Client-Side Data Fetching

Client-side data fetching happens in the browser after your page loads — exactly like traditional React applications. The browser requests data from an API, waits for the response, and then updates the component state. This pattern works perfectly for data that changes frequently, requires user authentication, or depends on user interactions. Imagine reading a news article where the comments section loads after the main content. The article itself might be static, but comments need real-time updates. Client-side fetching handles these dynamic requirements naturally, allowing your page to load quickly while additional content streams in progressively. The useEffect hook remains your primary tool for client-side data fetching in Next.js components. However, Next.js enhances this experience with automatic code splitting and optimized bundling that reduces the JavaScript payload sent to browsers.
// pages/articles/[slug].js - NewsWave article page with dynamic comments
import { useState, useEffect } from 'react';

export default function ArticlePage({ article }) {
  const [comments, setComments] = useState([]); // Comments state starts empty
  const [loading, setLoading] = useState(true);  // Track loading status

  useEffect(() => {
    // Fetch comments after the page loads in the browser
    fetch(`/api/comments?articleId=${article.id}`)
      .then(res => res.json())                    // Parse JSON response
      .then(data => {
        setComments(data.comments);               // Update state with comments
        setLoading(false);                        // Hide loading indicator
      });
  }, [article.id]);                              // Re-fetch if article changes

  return (
    <div>
      <h1>{article.title}</h1>
      <p>{article.content}</p>
      
      {loading ? (
        <p>Loading comments...</p>              
      ) : (
        <div>
          {comments.map(comment => (             // Render fetched comments
            <div key={comment.id}>{comment.text}</div>
          ))}
        </div>
      )}
    </div>
  );
}
localhost:3000 — NewsWave

What just happened?

The article content loads immediately (server-rendered), but comments fetch separately in the browser. The loading state provides user feedback while the API request completes. Try this: Notice how the comments appear after a delay, simulating real network requests.

Client-side fetching shines when you need to respond to user actions or display personalized content. Search functionality, user dashboards, and interactive features all benefit from this approach. The trade-off comes in the form of loading states and potential layout shifts as content arrives asynchronously.

SWR for Better Client-Side Fetching

SWR (Stale-While-Revalidate) represents Vercel's answer to complex client-side data fetching scenarios. The library handles caching, revalidation, error recovery, and loading states automatically. Think of SWR as a smart assistant that remembers previous API responses and updates them in the background without disrupting the user experience. The name "Stale-While-Revalidate" describes its core strategy perfectly. When you request data, SWR immediately returns cached results (even if slightly stale) while simultaneously fetching fresh data in the background. This approach eliminates loading spinners for repeat visits and provides instant perceived performance. SWR particularly excels in scenarios where multiple components need the same data. Instead of making duplicate API calls, SWR deduplicates requests and shares responses across your entire application. NewsWave benefits from this when displaying article previews, author information, or category data across different pages.
# Install SWR in your NewsWave project
npm install swr
Terminal
$ npm install swr
+ swr@2.2.4
✓ SWR installed successfully
// components/TrendingArticles.js - NewsWave trending section with SWR
import useSWR from 'swr';

// Fetcher function that SWR uses to make API calls
const fetcher = (url) => fetch(url).then(res => res.json());

export default function TrendingArticles() {
  const { data, error, isLoading } = useSWR(
    '/api/articles/trending',  // API endpoint to fetch
    fetcher,                   // Function that makes the actual request
    {
      refreshInterval: 30000,  // Refresh data every 30 seconds
      revalidateOnFocus: true  // Refresh when user focuses the tab
    }
  );

  if (error) return <div>Failed to load trending articles</div>;
  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      <h2>Trending Now</h2>
      {data.articles.map(article => (        // Render cached/fresh articles
        <div key={article.id}>
          <h3>{article.title}</h3>
          <span>{article.views} views</span>
        </div>
      ))}
    </div>
  );
}
localhost:3000 — NewsWave

What just happened?

SWR handled the loading state, cached the results, and configured automatic revalidation. The component updates every 30 seconds and refreshes when you switch browser tabs. Try this: SWR would show cached data instantly on subsequent renders, eliminating loading delays.

SWR transforms complex data fetching scenarios into simple, declarative code. The library handles edge cases like network errors, race conditions, and stale data automatically. Your components focus on rendering while SWR manages the entire data lifecycle behind the scenes.

Server-Side Data Fetching Introduction

Server-side data fetching represents one of Next.js's most powerful capabilities. Instead of waiting for JavaScript to load in the browser and then making API calls, your pages can fetch data on the server and arrive fully populated with content. This approach dramatically improves perceived performance and provides excellent SEO benefits since search engines see complete HTML content. Think of server-side fetching like a personal shopping service. Instead of giving customers a list of items to collect themselves, you gather everything they need before they arrive. The customer experience becomes seamless — they walk into a fully stocked, personalized environment tailored to their specific needs. Next.js provides several server-side data fetching methods, each optimized for different scenarios. Static generation builds pages at build time with pre-fetched data. Server-side rendering fetches data on each request. Incremental static regeneration combines the best of both approaches, updating static pages periodically with fresh data. The choice between these patterns depends on how often your data changes and when you need the freshest information. NewsWave articles might use static generation for published content but server-side rendering for personalized recommendations or real-time comment counts.

Data Fetching Patterns Comparison

Understanding when to use each data fetching pattern becomes crucial as your application grows in complexity. Each approach serves specific use cases and comes with distinct trade-offs in terms of performance, freshness, and server resources. The key lies in matching the right pattern to your content's characteristics and user expectations. Static generation works beautifully for content that rarely changes — think documentation, blog posts, or product catalogs. The pages build once and serve from a CDN, providing lightning-fast loading times worldwide. However, static pages struggle with personalized content or real-time updates that require fresh data on every visit. Server-side rendering excels when you need fresh data for every request but still want the SEO benefits of server-rendered HTML. User dashboards, personalized feeds, and location-based content all benefit from this approach. The trade-off comes in server processing time and the inability to cache responses effectively.

Client-Side Fetching

Perfect for user interactions, comments, personalized content. Data loads after page render.

Static Generation

Ideal for articles, documentation, marketing pages. Data fetched at build time for maximum speed.

Server-Side Rendering

Best for personalized dashboards, search results, real-time data. Fresh data on every request.

Incremental Static Regeneration

Combines static speed with fresh data. Pages regenerate periodically in the background.

The magic happens when you combine multiple patterns within a single application. NewsWave might use static generation for article content, server-side rendering for search results, and client-side fetching for user interactions like voting or bookmarking. Each piece of content gets the optimal fetching strategy.

Fetch API in Next.js

Next.js enhances the standard Fetch API with automatic request deduplication, caching, and performance optimizations. When multiple components request the same data during server-side rendering, Next.js intelligently makes only one network request and shares the result. This optimization prevents redundant API calls and reduces server processing time. The Fetch API works seamlessly across both server and client environments in Next.js. Your data fetching code looks identical whether it runs on the server during page generation or in the browser during user interactions. This consistency simplifies development and reduces the mental overhead of context switching between environments. Request deduplication becomes particularly valuable in complex page layouts where multiple components might need the same data. Imagine a NewsWave article page displaying author information in the header, sidebar, and footer. Instead of three separate API calls, Next.js makes one request and distributes the result to all components.
// lib/api.js - NewsWave API utility functions
export async function fetchArticle(slug) {
  const response = await fetch(`https://api.newswave.com/articles/${slug}`, {
    headers: {
      'Authorization': `Bearer ${process.env.API_KEY}`, // Server-side API key
      'Content-Type': 'application/json'
    },
    next: { 
      revalidate: 3600  // Cache for 1 hour (Next.js 13+ App Router)
    }
  });

  if (!response.ok) {
    throw new Error(`Failed to fetch article: ${response.status}`);
  }

  return response.json(); // Parse and return article data
}

export async function fetchAuthor(authorId) {
  const response = await fetch(`https://api.newswave.com/authors/${authorId}`, {
    next: { revalidate: 86400 } // Cache author data for 24 hours
  });

  return response.json();
}
Terminal
API request to https://api.newswave.com/articles/nextjs-data-fetching
✓ Article fetched and cached for 1 hour
Deduplicating 3 identical requests → serving from memory
// pages/articles/[slug].js - Using the API utility
import { fetchArticle, fetchAuthor } from '../../lib/api';

export async function getStaticProps({ params }) {
  try {
    const article = await fetchArticle(params.slug);    // Fetch article data
    const author = await fetchAuthor(article.authorId); // Fetch author data
    
    return {
      props: {
        article,
        author
      },
      revalidate: 3600 // Regenerate page every hour
    };
  } catch (error) {
    return {
      notFound: true // Show 404 if article doesn't exist
    };
  }
}

export default function ArticlePage({ article, author }) {
  return (
    <article>
      <h1>{article.title}</h1>
      <p>By {author.name}</p>
      <div>{article.content}</div>
    </article>
  );
}
localhost:3000/articles/nextjs-data-fetching — NewsWave

What just happened?

The page rendered with complete data immediately — no loading states. Next.js fetched the article and author data during build time, cached the requests, and served a fully populated HTML page. Try this: Notice how the content appears instantly without any client-side loading delays.

Modern browsers and Next.js work together to optimize these fetch requests through HTTP/2 multiplexing, connection pooling, and intelligent caching strategies. Your API calls become more efficient automatically, reducing server load and improving response times across your entire application.

Error Handling and Loading States

Robust data fetching requires comprehensive error handling and thoughtful loading states. Network requests can fail, APIs might be temporarily unavailable, and users might have slow connections. Graceful error handling transforms these potential frustrations into manageable user experiences that maintain trust and engagement. Loading states serve dual purposes — they provide user feedback during data fetching and prevent layout shifts when content arrives. Well-designed loading states use skeleton screens or placeholder content that matches the expected final layout. This approach maintains visual stability and reduces perceived loading time. Error boundaries and fallback content ensure your application remains functional even when specific data requests fail. Instead of breaking the entire page, isolated components can show error messages while the rest of the interface continues working normally. This resilience becomes crucial for production applications serving real users.
// components/ArticleCard.js - NewsWave article with error handling
import { useState, useEffect } from 'react';

export default function ArticleCard({ articleId }) {
  const [article, setArticle] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function loadArticle() {
      try {
        setLoading(true);                           // Show loading state
        setError(null);                             // Clear previous errors
        
        const response = await fetch(`/api/articles/${articleId}`);
        
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        
        const data = await response.json();
        setArticle(data);                           // Success: update state
      } catch (err) {
        setError(err.message);                      // Capture error message
        console.error('Article fetch failed:', err);
      } finally {
        setLoading(false);                          // Hide loading state
      }
    }

    loadArticle();
  }, [articleId]);
  // Render different states based on current status
  if (loading) {
    return (
      <div className="article-card skeleton">     {/* Skeleton loading state */}
        <div className="skeleton-title"></div>
        <div className="skeleton-author"></div>
        <div className="skeleton-excerpt"></div>
      </div>
    );
  }

  if (error) {
    return (
      <div className="article-card error">        {/* Error state with retry */}
        <h3>Unable to load article</h3>
        <p>{error}</p>
        <button onClick={() => window.location.reload()}>
          Try Again
        </button>
      </div>
    );
  }

  return (
    <div className="article-card">              {/* Success state */}
      <h3>{article.title}</h3>
      <p>By {article.author}</p>
      <p>{article.excerpt}</p>
    </div>
  );
}
localhost:3000 — NewsWave

What just happened?

The skeleton loading states maintain layout stability while data loads. Error boundaries would catch failed requests and show recovery options. Try this: Click "Reload Demo" to see the loading states again — notice how the skeletons prevent layout shift.

Professional applications layer multiple error handling strategies. Exponential backoff retries failed requests automatically. Circuit breakers prevent cascading failures. Fallback data ensures users always see something useful, even when live data becomes unavailable. These patterns create resilient user experiences that handle real-world network conditions gracefully.

Performance Tip

Always implement loading states that match your final content layout. Skeleton screens prevent cumulative layout shift (CLS) and provide better Core Web Vitals scores. Users perceive skeleton loading as 23% faster than traditional spinners, according to usability research.

Data fetching forms the foundation of modern Next.js applications. Master these patterns and your applications will feel fast, reliable, and professional. The next lesson explores getStaticProps in detail — the cornerstone of static generation that makes your pages load at the speed of light.

Quiz

1. Your NewsWave homepage displays trending articles in multiple components. What advantage does SWR provide over regular fetch calls?


2. What is the main characteristic of client-side data fetching in Next.js?


3. How should you handle loading states to provide the best user experience in NewsWave?


Up Next: getStaticProps

Dive deep into static site generation with getStaticProps — the function that pre-renders NewsWave pages at build time for lightning-fast loading and perfect SEO.