DEV Community

Cover image for Day 55 of #100DayOfCode — Data Fetching and Caching in NextJS
M Saad Ahmad
M Saad Ahmad

Posted on

Day 55 of #100DayOfCode — Data Fetching and Caching in NextJS

Data fetching with React follows the same well-known drill: useEffect, useState, loading spinners, and API keys being dangerously exposed on the frontend. Next.js changes all of that. With its built-in server-side data fetching and automatic caching system, Next.js makes it possible to build fast, secure, full-stack applications without reaching for a dozen extra libraries.

For day 55, the goal was to understand how data fetching works in Next.js, how it compares to the React way, and how to use the different caching strategies to make the app as fast and efficient as possible.


Data Fetching: React vs Next.js

How React Fetches Data

In React, data fetching happens entirely in the browser. When a user visits a page, the browser first downloads the HTML (which is mostly empty), then runs your JavaScript bundle, and only then fires off an API request to fetch the data. This means the user always sees a loading state before the actual content appears.

Here is what that typically looks like:

import { useState, useEffect } from 'react'

function Posts() {
  const [posts, setPosts] = useState([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetch('https://api.example.com/posts')
      .then(res => res.json())
      .then(data => {
        setPosts(data)
        setLoading(false)
      })
  }, [])

  if (loading) return <p>Loading...</p>

  return (
    <div>
      {posts.map(post => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </div>
      ))}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

This works, but it comes with real problems:

  • Loading spinners everywhere —> users see an empty page or spinner before any content appears.
  • Multiple round trips —> the browser loads the page, then makes a separate API request, adding latency.
  • Exposed API keys —> any secret keys used in fetch calls are visible in the browser's DevTools Network tab.
  • No built-in caching —> every render triggers a fresh API call unless you manually set up something like React Query or SWR.
  • SEO suffers —> search engines crawl the empty HTML before JavaScript runs, so they may not see your content at all.

How Next.js Fetches Data

Next.js introduces Server Components; components that run on the server before the HTML is ever sent to the browser. Because they run on the server, you can make them async and await your data at the top level. No useEffect, no useState, no loading spinner.

async function Posts() {
  const res = await fetch('https://api.example.com/posts')
  const posts = await res.json()

  return (
    <div>
      {posts.map(post => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </div>
      ))}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

That is the entire component. The server fetches the data, builds the HTML with the data already inside it, and sends the finished page to the browser. The user never sees a loading state because by the time the page arrives, the data is already there.

This approach is better for several reasons:

  • No loading spinners —> data arrives with the page, not after it.
  • Better performance —> one round trip instead of two.
  • API keys stay hidden —> the fetch call never reaches the browser, so secrets stay on the server.
  • Better SEO —> search engines receive fully rendered HTML with real content.
  • Built-in caching —> Next.js automatically caches fetch responses on the server (more on this below).

The fetch syntax is the same as in React or plain JavaScript. The only difference is where it executes, on the server, and the automatic handling by Next.js.


Comparison of Data Fetching between React/JS vs Next.js

Date Fetching React vs Next.js


What is Caching in Next.js?

Caching means storing the result of an API call so that the next time the same data is needed, it can be served instantly from the stored copy instead of making another network request. Without caching, every user visiting your page triggers a fresh API call, which is slow and expensive, especially if thousands of users are hitting the same endpoint.

Next.js extends the native fetch API with its own caching layer baked in. You control the caching behavior by passing options to your fetch call.

Important version note: In Next.js 13 and 14, caching was ON by default and you had to opt out. In Next.js 15, the default was flipped, caching is now OFF by default, and you have to opt in. Always check your version before assuming the default behavior.


Types of Caching in Next.js

1. Cache Forever (force-cache)

const res = await fetch('https://api.example.com/posts', {
  cache: 'force-cache'
})
Enter fullscreen mode Exit fullscreen mode

This fetches the data once and caches it indefinitely. Subsequent requests for the same data are served instantly from the cache without hitting the API again. This is the most aggressive caching strategy and gives you the fastest possible response times.

2. No Cache (no-store)

const res = await fetch('https://api.example.com/posts', {
  cache: 'no-store'
})
Enter fullscreen mode Exit fullscreen mode

This completely disables caching. Every request hits the API fresh and gets the latest data. This is the slowest option but guarantees you always have up-to-date information.

3. Revalidate Every N Seconds (ISR)

const res = await fetch('https://api.example.com/posts', {
  next: { revalidate: 60 }
})
Enter fullscreen mode Exit fullscreen mode

This is the sweet spot for most applications. It caches the response but automatically refreshes it every 60 seconds in the background. Users always get a fast cached response, but the data does not go stale forever. This is called Incremental Static Regeneration (ISR) and it uses a stale-while-revalidate strategy, serve the cached version immediately, then quietly fetch a fresh copy in the background for the next request.

4. Revalidate On Demand

import { revalidatePath } from 'next/cache'

// Call this function whenever your data changes
export async function updatePost() {
  // ... update logic

  revalidatePath('/posts') // tells Next.js to rebuild this page
}
Enter fullscreen mode Exit fullscreen mode

Instead of refreshing on a time interval, you manually tell Next.js when the cached data is stale. This is useful when you know exactly when data changes — for example, when a user submits a form, a CMS editor publishes new content, or a webhook fires from an external service. The page is rebuilt on the next request after revalidatePath is called.


Which Caching Strategy Should Be Used?

Choosing the right caching strategy depends entirely on how often the fetched data in the app changes and how critical it is that users see the latest version. Here is a practical breakdown:

Use force-cache for content that rarely changes

Think documentation pages, marketing landing pages, legal pages, blog posts that are rarely edited, or any content that was written once and stays the same. Since this data does not change, there is no reason to ever hit the API again after the first fetch. force-cache gives you maximum speed with zero cost.

const res = await fetch('https://api.example.com/docs', {
  cache: 'force-cache'
})
Enter fullscreen mode Exit fullscreen mode

Use revalidate (ISR) for content that changes occasionally

This is the right choice for the majority of real-world pages: e-commerce product listings where prices update periodically, news articles, blog indexes, weather data for a specific city, or sports standings. The data changes, but not so frequently that a slightly stale version causes real problems. A revalidation interval of 60 seconds to an hour is usually appropriate, depending on how time-sensitive the data is.

// Refresh every 10 minutes — good for product listings
const res = await fetch('https://api.example.com/products', {
  next: { revalidate: 600 }
})
Enter fullscreen mode Exit fullscreen mode

Use revalidatePath for content driven by user actions or a CMS

When you are building a blog or CMS-backed site and an editor publishes a new post, you want that page to update immediately, not in 10 minutes. revalidatePath lets you trigger a rebuild the moment the content changes. The same applies to e-commerce admin panels, user-generated content, or any situation where a specific action is the trigger for stale data.

import { revalidatePath } from 'next/cache'

async function publishPost(id) {
  await db.post.update({ where: { id }, data: { published: true } })
  revalidatePath('/blog') // immediately mark the blog listing as stale
}
Enter fullscreen mode Exit fullscreen mode

Use no-store for data that must always be live

User-specific data like dashboards, account settings, or notification counts must never be cached; each user needs their own fresh data. The same goes for live stock prices, real-time sports scores, or anything where a stale response would cause real user confusion or financial consequences.

const res = await fetch(`https://api.example.com/user/${userId}/dashboard`, {
  cache: 'no-store'
})
Enter fullscreen mode Exit fullscreen mode

Quick Reference

Data type Strategy Example
Static content, never changes force-cache Docs, legal pages, about page
Changes occasionally revalidate: 3600 Blog posts, product listings
Changes frequently revalidate: 60 News feed, prices, scores
Changes unpredictably revalidatePath on demand CMS content, user-published posts
Must always be live no-store User dashboard, live data, notifications

Final Thoughts

Data fetching and caching in Next.js simplify web application development. Instead of complex client-side fetching with useEffect, Next.js manages loading states, API key security, and caching out of the box.

With Server Components, you can use async/await data fetching directly in your components with minimal boilerplate. The built-in caching system allows for fine-grained control over data freshness in your fetch calls. These features enable you to build fast, secure, and SEO-friendly full-stack applications in Next.js while maintaining a great developer experience.

Thanks for reading. Feel free to share your thoughts!

Top comments (0)