A Comprehensive Guide to Data Fetching in Next.js 15
Table of contents
Data fetching in Next.js has come a long way, and with the introduction of the App Router and server components, we’ve stepped into a new era. The focus is shifting - Vercel now recommends doing as much data fetching on the server as possible. But that doesn’t mean the old ways are obsolete.
Next.js is all about flexibility, and there’s no single “best” way to fetch data - it’s about picking the right tool for the job. In this guide, we’ll break down the major data-fetching methods Next.js currently offers as of version 15, from server components to client libraries, and help you understand when to use each. Whether you're building a dynamic app or a fast static site, there’s an approach that fits. Let’s dive in.
TLDR - here’s a table:
Method | When to Use | Key Characteristics | How it's Invoked | Use in New Projects |
Server Components | For rendering UI on the server and performing server-side data fetching directly in components. | Supports server-side rendering, built for performance and scalability. | No special invocation, use async data fetching in components (server-side only). | Yes |
getServerSideProps (SSR) | For dynamic content that changes frequently and needs to be fetched on every request. | Executes on each request, ensuring fresh data but with potential for slower response times. | Use the `getServerSideProps` function in your page components. | No |
getStaticProps (SSG) | For static content that can be pre-rendered at build time, offering faster page loads. | Pre-renders pages at build time, excellent for static sites. | Use the `getStaticProps` function in your page components. | No |
Incremental Static Regeneration (ISR) | When static content needs to be updated at regular intervals without full rebuilds. | Allows pages to be revalidated based on time, combining static and dynamic capabilities. | Use `revalidate` option inside `getStaticProps` or `unstable_cache` for specific intervals. | Yes |
generateMetadata & generateStaticParams | For generating metadata or dynamic routes during build time or static generation. | Used for SEO and dynamic routing, allows pages to remain static while changing based on URL. | Use the `generateMetadata` and `generateStaticParams` functions in components. | Yes |
Server Actions | For handling mutations directly on the server, ideal for form submissions or similar actions. | Offers a new way to handle server-side mutations without exposing client-side logic. | No special invocation, use `Server Actions` in server components directly. | Yes |
Client-side Fetching | When you need to fetch dynamic data that should be rendered only on the client side. | Fetches data entirely on the client, useful for dynamic, user-interactive pages. | Use `useEffect` or any client-side libraries (e.g., `fetch`, `axios`) in components. | No (unless strictly needed) |
SWR (Stale-While-Revalidate) | For client-side data fetching with built-in caching, revalidation, and focus-tracking. | Provides a structured way to fetch, cache, and revalidate data on the client. | Use the SWR library by invoking `useSWR` in client-side components. | Yes |
TanStack Query (formerly React Query) | For client-side data fetching, caching, and syncing with built-in retries and background refetching. | Advanced caching, automatic retries, background updates, and mutation syncing. | Use in client-side components by invoking useQuery to manage server state in a more structured way. | Yes |
1. Server Components (App Router)
Level: The New Normal
Welcome to the App Router. Server Components are where Next.js is headed, and for good reason. You fetch data directly inside your component, no need for clunky wrappers like getServerSideProps
. Everything happens server-side, and the result is fast, scalable, and simple.
Why use it?
You want simplicity. No more middleman functions.
It’s fast and optimized for server-side performance.
You’re starting a new project. Use this by default.
How to invoke it:
// app/page.tsx
export default async function Page() {
const data = await fetch('https://api.example.com/posts');
const posts = await data.json();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Recommended for greenfield projects:
Yes. If you're starting fresh, this is the best way to go.
2. getServerSideProps (SSR) 🚫
Level: Old Reliable but Tired
Once the MVP of Next.js data fetching, getServerSideProps
still serves its purpose, but it’s tied to the legacy Pages Router. It’s great for ensuring fresh data on every request, but it’s also clunkier and less efficient than the App Router’s Server Components.
Why use it?
You’re stuck using the Pages Router for some reason.
You need fresh data on every request, like for real-time dashboards or personalized content.
How to invoke it:
// pages/index.tsx
export async function getServerSideProps() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return {
props: {
posts
}
};
}
const Page = ({ posts }) => (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
export default Page;
Recommended for greenfield projects:
No. Move on to the App Router if you can.
3. getStaticProps (SSG) 🚫
Level: Once Great, Now Legacy
Remember the days of static site generation (SSG)? getStaticProps
pre-renders your page at build time. It's still a solid option if you're stuck in the Pages Router, but for most new projects, you should be using Server Components or Incremental Static Regeneration (ISR).
Why use it?
You need build-time static generation.
You don’t need the content to be updated on every request.
How to invoke it:
// pages/index.tsx
export async function getStaticProps() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return {
props: {
posts
},
revalidate: 10 // Optional ISR
};
}
const Page = ({ posts }) => (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
export default Page;
Recommended for greenfield projects:
No. Leave it in the Pages Router past where it belongs.
4. Incremental Static Regeneration (ISR)
Level: Static with a Pulse
ISR lets you have your static cake and eat it too. Your pages get pre-rendered at build time, but they can be revalidated on demand, giving you the best of both worlds. It’s a key feature if you need a mix of static and dynamic content.
Why use it?
You want pre-rendered static pages with the ability to update periodically or on demand.
You like the idea of static performance with dynamic flexibility.
How to invoke it:
// app/page.tsx (ISR with App Router)
import { unstable_cache } from 'next/cache';
export default async function Page() {
const getPosts = unstable_cache(async () => {
const res = await fetch('https://api.example.com/posts');
return res.json();
}, ['posts'], { revalidate: 3600 });
const posts = await getPosts();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Recommended for greenfield projects:
Yes. Combine it with Server Components, and you’re golden.
5. generateMetadata & generateStaticParams
Level: SEO Wizardry
Need metadata for SEO or want to generate routes at build time? This is the Next.js way. You can fetch metadata and dynamically generate routes during static site generation in a clean, optimized way.
Why use it?
You’re building an app with dynamic routes.
You need SEO-friendly metadata based on your fetched content.
How to invoke it:
// app/posts/[id]/page.tsx
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(res => res.json());
return posts.map(post => ({
id: post.id,
}));
}
export async function generateMetadata({ params }) {
const post = await fetch(`https://api.example.com/posts/${params.id}`).then(res => res.json());
return {
title: post.title,
};
}
Recommended for greenfield projects:
Yes. Dynamic routes, SEO, and App Router? Absolutely.
6. Server Actions
Level: The Future is Now
Server Actions are new in Next.js 15 and allow you to handle mutations directly on the server - no API routes needed. This is great for things like form submissions or database writes without exposing your logic to the client.
Why use it?
You want to handle server-side mutations without creating API routes.
You’re using the App Router and prefer to keep things simple.
How to invoke it:
// app/page.tsx
export default function Page() {
async function handleSubmit(formData) {
'use server';
// Handle form data or mutate server-side data here
}
return (
<form action={handleSubmit}>
<input name="message" type="text" />
<button type="submit">Send</button>
</form>
);
}
Recommended for greenfield projects:
Yes. If you need server-side mutations, this is your new best friend.
7. Client-Side Fetching 🚫
Level: Still Hanging On
Sometimes, you just need to fetch data on the client, especially when you're dealing with highly interactive pages or data that changes after the initial load. But remember: in Next.js, server-side fetching is the first choice for performance reasons.
Why use it?
You need to fetch dynamic data after the initial page load.
Your use case involves real-time data or highly interactive components.
How to invoke it:
'use client'
import { useState, useEffect } from 'react';
export function Page() {
const [posts, setPosts] = useState([]);
useEffect(() => {
async function fetchPosts() {
const res = await fetch('https://api.example.com/posts');
const data = await res.json();
setPosts(data);
}
fetchPosts();
}, []);
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Recommended for greenfield projects:
No. Stick to server-side unless you really need client-side data fetching.
8. SWR (Stale-While-Revalidate)
Level: Client Fetching, but Smarter
Use SWR for client-side data fetching when you need features like automatic revalidation, caching, or stale content updates. SWR stands for Stale-While-Revalidate, a pattern where an HTTP header attribute allows caches to serve a stale asset while it's being revalidated.
Why use it?
You need to fetch dynamic data after the initial page load.
Your use case involves real-time data or highly interactive components.
How to invoke it:
Use the useSWR
hook in client components:
'use client'
import useSWR from 'swr';
// Define a fetcher function to be used with SWR
const fetcher = (url: string) => fetch(url).then(res => res.json());
export default function Posts() {
// Use SWR to fetch data from an API endpoint
const { data, error } = useSWR('/api/posts', fetcher);
// Handle error state
if (error) return <div>Failed to load posts</div>;
// Handle loading state
if (!data) return <div>Loading...</div>;
// Render the fetched posts
return (
<ul>
{data.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Recommended for greenfield projects:
Yes. If your project requires advanced client-side fetching and you want to stay within the Vercel ecosystem, this is the tool for you.
9. TanStack Query (formerly React Query)
Level: Client-side Fetching with Extra Features
TanStack Query, formerly known as React Query, is an excellent client-side data-fetching library similar to SWR but earlier with more advanced features, such as pagination, caching, automatic retries, and background refetching. It’s particularly useful when you need fine-grained control over your fetching, error handling, or mutations. Plus, Tanner Linsley and crew are brewing a rich ecosystem with cool projects like TanStack Start (currently in Alpha at the time of writing).
Why use it?
You need client-side data-fetching with features like caching, background updates, retries, and pagination.
You want to handle complex UI state management while fetching data.
You prefer advanced control over how your data is fetched, cached, and refetched.
How to invoke it:
To get started with TanStack Query, you need to install it:
npm install @tanstack/react-query
Then, wrap your app in the QueryClientProvider
and use the useQuery
hook in your component:
// app/page.tsx
'use client'
import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query';
// Create a client
const queryClient = new QueryClient();
const fetchPosts = async () => {
const res = await fetch('https://api.example.com/posts');
return res.json();
};
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Posts />
</QueryClientProvider>
);
}
function Posts() {
// Use the useQuery hook to fetch data
const { data, error, isLoading } = useQuery(['posts'], fetchPosts);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Failed to load posts</div>;
return (
<ul>
{data.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Recommended for greenfield projects:
Yes. If your project requires advanced client-side fetching with more control and complexity, TanStack Query is a solid choice.
Learn more:
Practical React Query is an indispensable guide for learning both React Query and data fetching in general.
Wrapping Up
Server components are clearly the future of data fetching in Next.js, and Vercel’s team has made it pretty clear: whenever you can, fetch your data on the server. It streamlines things and makes for faster, more efficient apps. But here’s the catch: there isn’t a universal "best way" to fetch data. It’s really about what fits your specific use case. Sometimes, server components are the ideal solution, while other times, you may need TanStack Query to handle complex client-side caching.
The important thing is having options. Next.js doesn’t lock you into one path - it gives you the flexibility to choose the right approach based on what your app needs. So, think of it less like picking the perfect method and more like finding the right tool for the job at hand.
Subscribe to my newsletter
Read articles from Benjamin Temple directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by