Next.js: Data Fetching in Next.js
Leeting Yan
A calm, practical guide to loading, caching, and streaming data in modern React applications.
Data fetching is one of the biggest changes in the Next.js App Router era.
Instead of manually orchestrating waterfalls, suspense boundaries, and client-side fetches, Next.js encourages a server-first, cached-by-default model that results in faster and more predictable applications.
But the shift introduces new concepts:
- Where does
fetch()actually run? - When does Next.js cache responses?
- What is “revalidation”?
- How do you fetch data per segment?
- What’s the right pattern for production apps?
This chapter provides a complete, practical understanding — calm, clear, and grounded.
The new mental model: “Server-first”
In the App Router:
- Components are Server Components by default
- Data fetching runs on the server
fetch()is enhanced with automatic caching and memoization- Rendering can be streamed to the client
This creates a simple rule of thumb:
Fetch data in Server Components unless you absolutely need client-side interactivity.
You get:
- Faster loads
- Smaller bundles
- No loading waterfalls
- Automatic caching
- Stronger guarantees
Client-side fetching is still possible, but it’s usually the exception.
Using fetch() in Server Components
A basic example:
export default async function Page() {
const res = await fetch("https://api.example.com/stats");
const data = await res.json();
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
Important:
- This code runs only on the server
- Nothing leaks to the browser
- No JS bundle is created
- Data loads before rendering
This is simpler and more powerful than old getServerSideProps.
Built-in caching behavior
By default, fetch() responses are cached automatically.
Example:
await fetch(url); // Cached forever by default
This is equivalent to:
await fetch(url, { cache: "force-cache" });
This means:
- Static pages load even faster
- External APIs are memoized
- Data does not refetch on every request
To fetch fresh data, you must opt out.
Opting out of the cache
Use:
fetch(url, { cache: "no-store" });
This disables caching and re-fetches on every request.
Use cases:
- Admin dashboards
- User-specific content
- Rapidly changing data
- Protected APIs
Example:
const orders = await fetch("/api/orders", { cache: "no-store" });
Simple and explicit.
Revalidation: “Static, but refreshable”
Revalidation allows cached data to refresh in the background.
Example:
await fetch(url, {
next: { revalidate: 60 } // refresh every 60 seconds
});
This creates a flow:
- First request → data generated (cached)
- Later request (within 60s) → cached
- After 60s → Next.js regenerates the data in the background
This is the basis of Incremental Static Regeneration (ISR) in App Router.
Useful for:
- Blogs
- Product pages
- Marketing content
- Semi-live dashboards
You get speed + freshness without server-side cost on every request.
Request Memoization: eliminating waterfalls
React automatically memoizes identical requests during a render pass.
Example:
const user1 = await fetch("/api/user/123");
const user2 = await fetch("/api/user/123");
This triggers:
- One actual network call
- All others return instantly from memory
This prevents data waterfalls inside complex UI trees.
You can safely fetch the same data in different components without worrying about performance.
Async Server Components
Server Components can be fully async:
export default async function Page() {
const product = await getProduct();
return <ProductCard product={product} />;
}
This leads to natural, readable code:
- No wrappers
- No special lifecycle methods
- No
useEffectfor data loading
Just async functions.
Using Suspense for parallel data fetching
You can parallelize multiple fetches with React Suspense:
import { Suspense } from "react";
export default function Page() {
return (
<>
<Suspense fallback={<div>Loading profile...</div>}>
<Profile />
</Suspense>
<Suspense fallback={<div>Loading activity...</div>}>
<Activity />
</Suspense>
</>
);
}
Each child loads independently, leading to smoother UX.
Combining Suspense + Server Components gives you:
- True parallel fetching
- Streaming UI
- Lower time to first byte
Data fetching in Client Components
Client Components fetch data using traditional patterns:
"use client";
import { useEffect, useState } from "react";
export default function Weather() {
const [data, setData] = useState(null);
useEffect(() => {
fetch("/api/weather")
.then((res) => res.json())
.then(setData);
}, []);
return <div>{JSON.stringify(data)}</div>;
}
Use cases:
- Realtime updates
- User-controlled refresh
- Reading browser APIs
- User-specific / interactive content
Client fetching is fine — but keep it intentional.
When to use each pattern
Use Server fetching when:
- Content is static or semi-static
- Data is shareable across users
- SEO matters
- You need to access secrets, DBs, APIs
- You want streaming or parallel fetching
- You want predictable UX
Use Client fetching when:
- You need stateful interactivity
- Polling, real-time updates
- UI depends on user preferences stored in browser
- Third-party embed scripts
- No SSR is required
Always choose server-first unless there’s a clear reason not to.
Organizing your data fetching
A common Birdor-style structure:
app/
dashboard/
page.tsx
lib/
api/
getUser.ts
getStats.ts
getPosts.ts
Example:
// lib/api/getStats.ts
export async function getStats() {
return fetch("https://api.example.com/stats", {
next: { revalidate: 300 },
}).then(res => res.json());
}
Then:
// app/dashboard/page.tsx
import { getStats } from "@/lib/api/getStats";
export default async function Page() {
const stats = await getStats();
return <Dashboard stats={stats} />;
}
Keeps UI clean and logic reusable.
Edge cases and pitfalls
❌ Putting use client on the entire route
Keep data loading in server components as much as possible.
❌ Fetching private data in client components
Client code cannot access environment variables safely.
❌ Forgetting about caching
If you expect new data each request, explicitly set cache: 'no-store'.
❌ Over-using revalidation
Set revalidate intervals thoughtfully — too frequent = unnecessary load.
Simplicity is your friend.
Summary
Data fetching in Next.js is powerful, flexible, and server-first by design:
fetch()runs on the server by default- Responses are cached automatically
- Revalidation keeps data fresh
- Suspense enables parallel fetching
- Client fetching is optional and intentional
This chapter forms the foundation for all real-world Next.js apps.
Understanding these mechanics leads to faster, more stable applications with less code.
Next, we’ll explore API Routes & Server Actions, the backbone for mutations and backend logic inside the App Router.