On the server, with fetch

Next.js extends the native fetch Web API to allow you to configure the caching and revalidating behavior for each fetch request on the server. React extends fetch to automatically memorize fetch requests while rendering a React component tree.

On the server, with third-party libraries

In cases where you’re using a third-party library that doesn’t support or expose fetch (for example, a database, CMS or ORM client), you can configure the caching and revalidating behavior of those requests using the Route Segment Config Option and React’s cache function.

On the client, via a Route Handler

If you need to fetch data in a client component, you can call a Route Handler from the client. Route Handlers execute on the server and return the data to the client. This is useful when you do not want to expose sensitive information to the client, such as API tokens.

On the client, with third-party libraries

You can also fetch data on the client using a third-party library such as SWR or TanStack Query. These libraries provide their APIs for memorizing requests, caching, revalidating, and mutating data.

 

 

Fetching Data on the Server 

Fetching data on the server is often the preferred approach. In fact, this practice is also recommended by the creators of Next.js. They highly recommend fetching server data with Server Components whenever possible. By choosing this way, you will:

Have Direct Access to Backend Data Resources

Fetching data on the server allows direct access to backend data resources, ensuring efficient retrieval and utilization of data without exposing sensitive information to the client. 

Enhanced Security

By fetching data on the server, sensitive information like access tokens and API keys remains secure, mitigating the risk of exposure to potential threats on the client side.

 

Consolidated Environment for Fetching and Rendering

Server-side data fetching enables data retrieval and rendering within the same environment, minimizing back-and-forth communication between the client and server and optimizing performance.

Efficient Data Fetching

Perform multiple data fetches with a single round-trip instead of multiple individual requests on the client. 

  • Reduce client-server waterfalls. 
  • Reduce latency and improve performance. 

 

Fetching Data Where It’s Needed 

If you need to use the same data in multiple components in a tree, you don’t have to fetch data globally, nor forward props between components. Instead, you can use fetch or React cache in the component that needs the data, without impacting the performance.

Fetch: The Fetch API provides an interface for fetching resources (including across the network). It is a more powerful and flexible replacement for XMLHttpRequest. The Fetch API uses Request and Response objects (and other things involved with network requests), as well as related concepts such as CORS and the HTTP Origin header semantics. For making a request and fetching a resource, use the fetch() method. It is a global method in both Window and Worker contexts. This makes it available in pretty much any context you might want to fetch resources. 

React Cache: React Cache is a library developed by the React team that provides a simple API for managing data fetching and caching in React applications. It allows you to cache data in memory, reducing the need to refetch data unnecessarily. React Cache helps optimize performance by storing data locally within components, making it readily available when needed without having to fetch it repeatedly. 

Streaming 

With streaming, you can instantly render parts of the page that do not specifically require data and show a loading state for parts of the page that are fetching data. This means that we have a non-blocking UI; users do not need to wait for the entire page to load before they can start interacting with it. 

To learn how Streaming works in React and Next.js, it’s helpful to understand Server-Side Rendering (SSR) and its limitations. With SSR, there’s a series of steps that need to be completed before a user can see and interact with a page:

First, all data for a given page is fetched on the server

The server then renders the HTML for the page

The HTML, CSS and JavaScript for the page are sent to the client

Finally, React hydrates the user interface to make it interactive

A non-interactive user interface is shown using the generated HTML and CSS

These steps are sequential and blocking, meaning the server can only render the HTML for a page once all the data has been fetched.  

Streaming allows you to break down the page’s HTML into smaller chunks and progressively send those chunks from the server to the client. 

This behavior can also be achieved by using plain React.js with React Query, Suspense, and ErrorBoundary. Next.js brings this to another level, reducing the amount of code (components), amount of hooks and improving performance with an advanced routing system. 

(Source next.js) 

Parallel and Sequential Data Fetching 

Data fetching strategies play a crucial role in optimizing performance. Sequential data fetching involves dependencies between requests, while parallel fetching can significantly decrease the total time needed to load data. In short:

  • Sequential Data Fetching – requests in the route are dependent on each other, thus, they create waterfalls. 
  • Parallel Data Fetching – requests are eagerly initiated and will load data at the same time. This will reduce waterfalls and the total time needed to load data. 

(Source next.js) 

My Favorite Way of Fetching Data, Mutating and Revalidating Data (When Feasible) 

Imagine you're building a house. You could either bring in all the materials and tools you need each time you want to work on a specific part of the house, or you could set up a workshop nearby where everything is already there and accessible whenever you need it. Server actions are like that workshop – they provide a dedicated space for fetching, updating, and ensuring data is up to date.

Server Actions are asynchronous functions executed on the server. They can be used in Server and Client Components to handle form submissions and data mutations in Next.js applications. 

In forms:

  • Server Components support progressive enhancement by default, a form can be submitted even if JavaScript hasn’t loaded yet or is disabled. 
  • In Client Components, Server Actions will queue submissions if JavaScript isn’t loaded yet, prioritizing client hydration. 

Server actions are not limited to <form> and can be invoked from event handlers, such as useEffect, third-party libraries, and other form elements like <button>. 

Server actions integrate with Next.js caching and revalidation architecture. When action is invoked, Next.js can return both the updated UI and new data in a single server roundtrip. 

Here is a code snippet from my current project. This logic must allow changing programs by selecting a program in the UI and confirming the selected program. Also, this action needs to immediately revalidate the path and the UI flow will change accordingly. 

 

import { revalidatePath } from ‘next/cache’; 

import { cookies } from ‘next/headers’; 

export async function changeProgram(programId: string) {
  const session = await getSessionWritable(cookies());
  if (!session.auth) throw new Error(‘Session not found.’);

  const { cardNumber } = session.auth.instructor;
  const instructorId = Number(cardNumber);

  // Find instructor.
  const instructor = await Instructor.find(instructorId);
  if (!instructor) throw new Error(‘Instructor not found.’);

  // Get Salesforce user info.
  const { programs } = await getUserProgramsByCardNumber(instructorId);

  // Find a program to change into.
  const program = programs.find(program => program.programId === programId);
  if (!program) throw new Error(‘Program not found.’);

  // Update database.
  const facility = Instructor.programToFacility(program);
  await Instructor.updateFacility(instructor, facility);

  // Update session cookie with the selected program.
  session.auth.instructor.program = program;
  await session.save();

  // Revalidate.
  revalidatePath(routes.root());

  return program;
} 

Since we are working with different external or legacy APIs/services, you can see we are using some services that I omitted from the imports, but the point here is that with simple action, we can change, replace, and remove some data, as well as fetch data, while also being able to revalidate entire path and use fresh data. 

Change Program Action usage 

The following function makes sure you’ve selected a different program than the one you’re currently working on. If you have, then it goes ahead and switches you to that new program using the changeProgram server action we created.

The following function makes sure you’ve selected a different program than the one you’re currently working on. If you have, then it goes ahead and switches you to that new program using the changeProgram server action we created.

const handleSaveProgram = async () => {
if (programSelected && program.programId !== programSelected.programId){
await changeProgram(programSelected.programId);
}
};

Revalidating Data 

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.

Data Cache

Imagine you have a shelf where you keep your books, but you want to make sure that the information in those books is always up to date. The "revalidation" process is like periodically checking those books to see if any information has changed. Now, let's say you find out that some of the information in those books has become outdated.

What do you do? You “purge” the outdated information from the books and go get the latest versions.

In the context of software, particularly in Next.js, the “Data Cache” is like your shelf of books, and “revalidation” is the process of checking if the data is still current. When the system detects that the data is outdated, it “purges” or removes the old data from the cache and then goes and “re-fetches” or retrieves the latest data from its source.

Cached data can be revalidated in two ways: 

Time-Based Revalidation: Automatically revalidate data after a certain amount of time has passed. This is useful for data that changes infrequently, and freshness is not as critical. 

fetch(‘https://…’, { next: { revalidate: 3600 } }) 

How time-based revalidation works? 

  • The first time a fetch request with revalidate is called, the data will be fetched from the external data source and stored in the Data Cache. 
  • Any requests that are called within the specified timeframe (e.g. 60 seconds) will return the cached data. 
  • After the timeframe, the next request will still return the cached (now stale) data. 
    • Next.js will trigger revalidation of the data in the background. 
    • Once the data is fetched successfully, Next.js will update the Data Cache with the fresh data. 
    • If the background revalidation fails, the previous data will be kept unaltered. 

On-Demand Revalidation 

Data can be revalidated on-demand by path (revalidatePath) or by cache tag (revalidateTag) inside a Server Action or Route Handler

How on-demand revalidation works: 

  • The first time a fetch request is called, the data will be fetched from the external data source and stored in the Data Cache. 
  • When an on-demand revalidation is triggered, the appropriate cache entries will be purged from the cache. 
    • This is different from time-based revalidation, which keeps the stale data in the cache until the fresh data is fetched. 
  • The next time a request is made, it will be a cache MISS again, and the data will be fetched from the external data source and stored in the Data Cache. 

With Cache Tag:

Next.js has a cache tagging system for invalidating fetch requests across routes. 

  • When using fetch, you have the option to tag cache entries with one or more tags. 
  • Then, you can call revalidateTag to revalidate all entries associated with that tag. 

// app/page.tsx 

export default async function Page() {
  const res = await fetch(‘https://…’, { next: { tags: [‘collection’] } })
  const data = await res.json()
  // …
} 

With Server Action 

// app/actions.ts 
 
‘use server’ 
 
import { revalidateTag } from ‘next/cache’ 
 
export default async function action() { 
  revalidateTag(‘collection’) 
} 

Conclusion 

It is a fact that any programming language, framework, or library is useful in its way. Depending on your needs and the client’s requirements, you shouldn’t limit yourself and choose the most suitable tools for your project. When it comes to Next.js, I find it very useful in cases when you want to mitigate the gap between the time you have and the time you need to develop a specific product. Of course, it has its strengths and weaknesses. 

For example, if you want to explore using Next.js 12 and decide to read online reviews of peers’ experiences, you will probably find many comments claiming that it’s only good for fast projects, hobby projects, blogs, e-commerce, etc. On the other hand, if you haven’t worked with Next.js 13, you may be shocked by all the changes in their API and start thinking you don’t even know what Next.js is. So, is it worth it?

In my personal experience, there are many strengths to using Next.js. When compared with React.js, with the latest version of Next.js and the App Router, you gain:

Reduced Development Time

Better Performance and Speed

More Robust Security

SEO

Maintenance Efficiency

This means you’ll be able to develop MVPs much faster, with better user experience, and security, and have scalability for future growth.

That said, I am not saying that Next.js is perfect. Once you start working with it, you may discover a few challenges. Some of the weaknesses of the latest version of Next.js are:

  • Development and Management – you will need a dedicated person with the proper knowledge and skills for Next.js and its ecosystem. 
  • Absence of plugins – there are still not many plugins available, which leads to not having as much freedom with the latest version. 
  • Learning Curve – if you haven’t had the opportunity to work with Next.js so far, prepare for a learning curve until you get the hang of things. 
  • Limited – It may only be suitable for certain projects and a good fit for a few developers. 

All in all, in my personal experience, Next.js is an excellent framework for data fetching and revalidating, with its pros heavily outweighing the cons. 

Author

Lazar Stankovic

Full Stack Engineer