A detailed Guide to data fetching in Next.js 14

A detailed Guide to data fetching in Next.js 14

Next.js has evolved rapidly since its inception, constantly introducing innovative ways to handle data fetching—a crucial aspect of modern web development.

This evolution aims to improve performance, enhance SEO, and provide developers with flexible options to suit various rendering strategies.

In this article, we delve into the progression of data fetching techniques in Next.js, from its early versions up to the latest advancements in version 14.

Earlier approach to data fetching with getInitialProps

In the early stages of Next.js, prior to version 9.3, getInitialProps was the go-to method for fetching data on the server side.

This function allowed developers to fetch necessary data for a page before it was rendered, ensuring that the server could deliver a complete HTML page to the client, pre-populated with all the required data. This approach significantly benefited SEO and initial page load times.

Consider fetching a list of blog posts for a homepage:

// Prior versions of Next.js utilized getInitialProps for server-side data fetching.

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

HomePage.getInitialProps = async () => {
	const res = await fetch('https://api.example.com/posts');
	const posts = await res.json();
	return { posts };
};

export default HomePage;

In this code snippet, getInitialProps fetches blog posts server-side, ensuring the data is ready before the page is served to the user.

Introducing getServerSideProps and getStaticProps in Next.js 9.3

With the release of Next.js 9.3, two new data fetching methods were introduced to replace getInitialProps: getServerSideProps and getStaticProps, catering to SSR and SSG respectively. These methods provide more control and efficiency in data fetching.

Server-Side Rendering with getServerSideProps

The getServerSideProps method fetches data on the server at request time. This method is suitable for pages that need to display frequently updated data.

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

export async function getServerSideProps() {
	// Fetch data from external API
	const res = await fetch('https://api.example.com/posts');
	const posts = await res.json();

	// Pass data to the page via props
	return { props: { posts } };
}

export default HomePage;

Next.js calls the getServerSideProps async function when rendering the page on the server. It fetches the data and then returns an object with a props key. The data fetched is then passed to the page component as props.

The getServerSideProps is only used if you need to render a page that relies on personalized user data, or information that can only be known at request time. For example, authorization headers or a geolocation.

If you do not need to fetch the data at request time, or would prefer to cache the data and pre-rendered HTML, that is why getStaticProps was introduced.

Static Site Generation with getStaticProps

getStaticProps allows you to fetch data at build time. This method is ideal for pages that can be pre-rendered with data that doesn't change often.

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

export async function getStaticProps() {
	// Fetch data during build time
	const res = await fetch('https://api.example.com/posts');
	const posts = await res.json();

	// Pass the fetched data to the page via props
	return { props: { posts } };
}

export default HomePage;

The getStaticProps function fetches the list of posts during the build time instead of fetching them on each request as getServerSideProps does. This method is beneficial for performance because it allows the page to be served as static HTML, which is faster than server-side rendered pages for each request.

When using the getStaticProps and you need to update the data frequently, there was an option to use Incremental Static Regeneration (ISR) by providing a revalidate time in the return of getStaticProps. This approach allows you to update static content after deployment without needing to rebuild the entire site.

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

export async function getStaticProps() {
	// Fetch data during build time
	const res = await fetch('https://api.example.com/posts');
	const posts = await res.json();

	// Pass the fetched data to the page via props, enable ISR
	return {
		props: { posts },
		// Here we specify the revalidation time in seconds. This means the page will be regenerated
		// at most once every 10 seconds if there are requests coming in for this page.
		// Change the time based on your application's needs.
		revalidate: 10,
	};
}

export default HomePage;

In this example, the revalidate property is set to 10, which means that the static page will be regenerated at most once every 10 seconds if there are new requests for the page. This allows your application to serve static pages for most requests, while still periodically updating the content based on the specified interval.

Evolution in Next.js 14: Beyond getServerSideProps and getStaticProps

Today with the introduction of Sever components in Next.js 13, a lot has changed in Next.js. For example you no longer need methods like getServerSideProps and getStaticProps for handling data fetching.

You can now use the Fetch Web API to perform fetch requests in your components with async/await:

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

  if (!res.ok) {
    throw new Error('Failed to fetch data');
  }

  return res.json();
}

const HomePage = () => {
  const posts = await fetchPosts()

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

export default HomePage;

This approach simplifies data fetching by utilizing standard web APIs and leveraging async/await for more readable asynchronous code, marking a significant shift in how data is handled in Next.js applications.

Caching in Next.js 14

Next.js 14 enhances the native fetch Web API, offering options to configure caching and revalidation behavior for fetch requests on the server.

By default, Next.js automatically caches the returned values of fetch in the Data Cache on the server. This means that the data can be fetched at build time or request time, cached, and reused on each data request.

// 'force-cache' is the default caching strategy
fetch('https://...', { cache: 'force-cache' });

For scenarios requiring fresh data on each request, the cache option can be set to 'no-store', bypassing the cache entirely:

fetch('https://api.example.com/posts', { cache: 'no-store' });

Additionally, Route Segment Config options allow developers to disable caching for specific route segments, influencing all data requests within that segment:

// Disable caching for this route segment
export const dynamic = 'force-dynamic';

View all the available cache options in the [fetch](https://nextjs.org/docs/app/api-reference/functions/fetch) API reference.

Revalidating data in Next.js 14

Revalidation is the process of purging the Data Cache and re-fetching the latest data. This is useful when your data changes and you want to ensure you show the latest information.

To revalidate data at a timed interval, you can use the next.revalidate option of fetch to set the cache lifetime of a resource (in seconds).

fetch('https://api.example.com/posts', { next: { revalidate: 3600 } });

Route Segment Config Options also allow for route-wide revalidation settings:

export const revalidate = 3600; // revalidate at most every hour

If you have multiple fetch requests in a statically rendered route, and each has a different revalidation frequency. The lowest time will be used for all requests. For dynamically rendered routes, each fetch request will be revalidated independently.

Learn more about time-based revalidation.

Data fetching in client components in Next.js 14

For client-side data fetching, Next.js 14 supports calling Route Handler or using third-party libraries like SWR or TanStack Query.

Let’s explore how to perform data fetching in Route Handlers.

Route Handlers execute on the server and return the data to the client. This is useful when you don't want to expose sensitive information to the client, such as API tokens.

Route Handlers allow you to create custom request handlers for a given route using the Web Request and Response APIs. They are defined in a route.js|ts file inside the app directory:

export async function GET(request: Request) {}

This setup ensures that HTTP methods such as GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS are supported, with unsupported methods returning a 405 Method Not Allowed response.

Suppose you have a route.ts file that fetches posts:

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

	return Response.json({ data });
}

You can then make a request to this server in your client component using various React hooks:

import { useState, useEffect } from 'react';

const HomePage = () => {
	const [posts, setPosts] = useState([]);
	const fetchPosts = async () => {
		try {
			const response = await fetch('https://localhost:3000/api', {
				method: 'GET',
			});
			if (response.ok) {
				const { posts } = await response.json();
				setPosts(posts);
			}
		} catch (error) {
			console.error(error);
		}
	};
	useEffect(() => {
		fetchPosts();
	}, []);

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

export default HomePage;

Conclusion

In this guide you have learned the evolution of data fetching in Next.js and how you can fetch data in Next.js 14’s server and client-side components.

If you are intersted in SSG for your Next.js 14 project and interact with dynamic routes, you may want to check out this guide on Statically generate dynamic routes at build time in Next.js 14.

Have fun coding!


Joel Olawanle

Written by Joel Olawanle

Joel is a software engineer and teacher. He write's about software development, productivity, and life. He has written over 150 technical content that has helped millions of people all over the world.


Previous Post

Statically generate dynamic routes at build time in Next.js 14

Learn how Next.js 14 handles static rendering when dealing with dynamic routes. If you're making the switch from older versions or just getting to gri...

Next Post

How To Clone a Specific Branch With Git

Learn how Next.js 14 handles static rendering when dealing with dynamic routes. If you're making the switch from older versions or just getting to gri...