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 = async () => {
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!