Practical Guide to Next.js fetch: Caching, No-Caching, and Revalidation (With Clear-Cut Examples)

Next.js has revolutionized data fetching with its powerful built-in fetch API. While fetch is part of JavaScript, Next.js extends it to give developers automatic caching, revalidation, and optimization features that help build faster and more efficient apps.

In this guide, we’ll explore practical examples of how to use fetch in Next.js 13+ (App Router) — covering three key cases:

  1. Fetch with caching (default behavior)
  2. Fetch without caching
  3. Fetch with revalidation (ISR-style)

Understanding fetch in Next.js

In Next.js (with the App Router), the fetch function is extended to support data caching and revalidation directly. This means you don’t have to manually use getStaticProps or getServerSideProps anymore — you can control caching behavior right inside your fetch call.


Example 1: Fetch with caching (default)

By default, all fetch requests in Server Components are cached and reused across requests — this makes them ideal for static or rarely changing data.

// app/page.js
export default async function Page() {
  const res = await fetch('https://api.example.com/posts', {
    cache: 'force-cache', // Cached by default (explicitly defined)
  });
  const posts = await res.json();

  return (
    <div>
      <h1>Blog Posts (Cached)</h1>
      <ul>
        {posts.map(post => <li key={post.id}>{post.title}</li>)}
      </ul>
    </div>
  );
}

What happens here:

  • Data is fetched once during build or first request.
  • Next.js caches the response globally.
  • Subsequent requests reuse the cached data.

This is similar to Static Site Generation (SSG) — super fast and efficient for static data.


Example 2: Fetch without caching

If your data changes frequently (like user dashboards, analytics, or real-time APIs), you can disable caching entirely.

Use cache: 'no-store':

// app/page.js
export default async function Page() {
  const res = await fetch('https://api.example.com/users', {
    cache: 'no-store',
  });
  const users = await res.json();

  return (
    <div>
      <h1>Users (No Cache)</h1>
      <ul>
        {users.map(user => <li key={user.id}>{user.name}</li>)}
      </ul>
    </div>
  );
}

What happens here:

  • Next.js skips caching completely.
  • The data is fetched fresh on every request.

This is equivalent to Server-Side Rendering (SSR).

Use this for real-time or user-specific data.


Example 3: Fetch with revalidation (ISR style)

Sometimes you want the best of both worlds — cache data but refresh it periodically. That’s where revalidate comes in.

Use the next: { revalidate: X } option (X = seconds):

// app/page.js
export default async function Page() {
  const res = await fetch('https://api.example.com/news', {
    next: { revalidate: 60 }, // Revalidate every 60 seconds
  });
  const news = await res.json();

  return (
    <div>
      <h1>Latest News (Revalidated every 60s)</h1>
      <ul>
        {news.map(item => <li key={item.id}>{item.title}</li>)}
      </ul>
    </div>
  );
}

What happens here:

  • The first request fetches and caches the data.
  • The cached data is reused for 60 seconds.
  • After 60 seconds, the next request triggers a background revalidation — updating the cache with fresh data.

This behaves like Incremental Static Regeneration (ISR).


Comparison Summary

Use CaseOptionBehaviorEquivalent To
Static data(default)Cached foreverSSG
Dynamic datacache: 'no-store'Always freshSSR
Periodically updated datanext: { revalidate: X }Cached + revalidatedISR

Bonus: Conditional Fetching Example

You can even combine logic based on environment or props:

export default async function Page({ searchParams }) {
  const shouldRevalidate = searchParams.preview === 'true';

  const res = await fetch('https://api.example.com/products', {
    cache: shouldRevalidate ? 'no-store' : 'force-cache',
    next: shouldRevalidate ? { revalidate: 10 } : undefined,
  });

  const products = await res.json();

  return (
    <div>
      <h1>Products</h1>
      {shouldRevalidate && <p>Preview mode: live updates enabled</p>}
      <ul>
        {products.map(p => <li key={p.id}>{p.name}</li>)}
      </ul>
    </div>
  );
}

Best Practices

  1. Use default caching for static or slow-changing content.
  2. Use revalidate for semi-dynamic data (e.g. news, prices).
  3. Use cache: 'no-store' for frequently changing or user-specific data.
  4. Keep data-fetching server-side whenever possible (Server Components are lighter and faster).

Conclusion

Next.js makes data fetching declarative, powerful, and optimized by default. By understanding how caching, revalidation, and no-cache modes work, you can strike the perfect balance between performance and freshness.

Tip: The built-in fetch caching system is one of the biggest advantages of the App Router — use it wisely to make your Next.js apps blazing fast.


In short:

  • fetch() → Cached (SSG)
  • fetch({ cache: 'no-store' }) → No cache (SSR)
  • fetch({ next: { revalidate: X } }) → Revalidated (ISR)

Master these three, and you’ve mastered Next.js data fetching!