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>
)
}
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
fetchcalls 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>
)
}
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
fetchsyntax 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
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'
})
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'
})
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 }
})
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
}
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'
})
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 }
})
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
}
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'
})
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)