MB
Next.js Data Fetching Techniques

Next.js Data Fetching Techniques

August 4, 2024

Next.js is a powerful React-based framework that simplifies web application development by providing server-side rendering (SSR) and static site generation (SSG) out of the box. As websites grow into full-fledged applications, efficient data management becomes crucial. The Next.js App Router enhances flexibility and control over data fetching, allowing developers to optimize performance and user experience.

Data Fetching in App Router

The Next.js App Router provides the React Server Components (RSC) architecture, enabling data fetching through server and client components.

Fetching data with server components offers significant benefits, as they can directly interact with server-side resources like databases and file systems.

This approach not only utilizes the server's computational power and proximity to data sources for efficient data fetching and rendering but also reduces the need for processing on the client side.

Let's assume this is the basic architecture of our application.

sh

my-next-app/
├── app/
├── layout.js
├── page.js
├── products/
│ ├── page.js
│ └── [id]/
│ └── page.js
├── chat/
│ └── [chatId]/
│ └── page.js
├── components/
├── ChatComponent.js
├── pages/
└── api/
├── revalidate.js
└── send-message.js
├── package.json
└── next.config.js


Server Components

The RSC architecture in the app router enables the use of async and await keywords within Server Components. By defining your component as an asynchronous function, you can utilize the familiar await syntax in JavaScript. This forms the foundation for data fetching in server components.

tsx

// app/product/page.tsx

// Define a TypeScript interface for the product data
interface Product {
id: number;
title: string;
price: number;
description: string;
category: string;
image: string;
}

// Next.js server component for the product page
export default async function ProductPage() {
// Fetch product details from the API
const res = await fetch('https://fakestoreapi.com/products/3', {
});

// Handle any fetch errors
if (!res.ok) {
throw new Error('Failed to fetch product details');
}

// Use the Product type to type the JSON response
const product: Product = await res.json();

return (
<div>
<h1>{product.title}</h1>
<img src={product.image} alt={product.title} width={200} />
<p>{product.description}</p>
<p>Price: ${product.price}</p>
<p>Category: {product.category}</p>
</div>
);
}

The component is defined as an async function, allowing the use of await for data fetching directly within the component. This modern syntax simplifies asynchronous operations, making the code cleaner and more readable.

tsx

export default async function ProductPage() {
const res = await fetch('https://fakestoreapi.com/products/3');
const product: Product = await res.json();
// Render product details
}

Using the fetch API, the component retrieves product data from the API. By leveraging SSR, it renders content on the server, providing a faster initial load and better SEO, as the HTML is ready when it reaches the client.

🚩 One caveat to be aware of is that the data may be statically generated at build time, meaning it won't update automatically unless the application is rebuilt. This can be addressed with strategies like Incremental Static Regeneration (ISR) to ensure data freshness.


Caching

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.

tsx

// 'force-cache' is the default, and can be omitted
fetch('https://...', { cache: 'force-cache' })

This can lead to faster responses as data might be stored temporarily, reducing the need to fetch it again from the origin server. However, it can also serve stale data if not configured properly.

To control caching behavior explicitly, you can use options such as 'no-store' to disable caching or 'force-cache' to ensure data is cached:

tsx

/**
* By setting the cache option to 'no-store',
* we ensure that fresh data is fetched
* every time the page is requested.
*/
fetch('https://...', { cache: 'no-store' });

Setting the cache to 'no-store' ensures that fresh data is fetched on every request, which is crucial for applications that require real-time data accuracy.


Revalidation (ISR)

In production, data fetched during SSR might be static and not update automatically. Next.js's Incremental Static Regeneration (ISR) offers a solution by allowing pages to be regenerated in the background as traffic comes in.

By setting a revalidate property in the object from a data-fetching function, you can specify how often to refresh the page:

tsx

export default async function ProductPage() {
const res = await fetch('https://fakestoreapi.com/products/3',
{ next:
{
revalidate: 60
//re-fetching the latest data in 60 sec
}
});
const product: Product = await res.json();
// Render product details
}

ISR provides a balance between static generation and dynamic updates, ensuring that users receive the latest content without the need for a full site rebuild.


Use Cases Of Using ISR and no-store

  • ISR is ideal for content that doesn’t change every second but needs to be updated periodically, such as blogs, product pages, or news sites where data is relatively stable but requires occasional updates.
  • no-store is more suitable for content that needs to be real-time and cannot tolerate any staleness, such as live sports scores, stock market data, or real-time dashboards.


Using Webhooks for On-Demand Content Updates

In addition to Incremental Static Regeneration (ISR) and the no-store caching strategy, webhooks offer a powerful alternative for managing content updates in a Next.js application. By leveraging webhooks, you can ensure content is statically generated and only updated on demand, reducing unnecessary server load and providing precise control over when content gets refreshed.

Let's assume we are using a headless CMS like Contentful, Sanity, or WordPress, which sends a webhook to our Next.js application whenever new content is published.

The webhook will trigger a revalidation of the specific page to ensure it displays the latest content.

Create an API route in your Next.js application to handle the incoming webhook request. This will typically be located in the pages/api directory of your project.

typescript

// pages/api/revalidate.ts

import { revalidatePath } from 'next/cache';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';

export async function POST(request: NextRequest): Promise<NextResponse> {
try {
const { item } = await request.json();

const path = item?.get?.tree?.path;

if (path) {
/**
* revalidatePath is used to manually refresh the data and re-render
* the route segments under a specific path. This function updates
* the Data Cache and also invalidates the Full Route Cache.
*/
revalidatePath(path);

return NextResponse.json({ revalidated: true, now: Date.now() });
}

return NextResponse.json({
revalidated: false,
now: Date.now(),
message: 'Missing path to revalidate',
});
} catch (error) {
return NextResponse.json({ error: 'Error revalidating', details: error }, { status: 500 });
}
}

revalidatePath offers developers precise control over content updates in a Next.js application. By allowing on-demand revalidation of specific paths, it enhances the flexibility and responsiveness of static sites, ensuring that users always have access to the latest content with minimal delay. This feature is essential for applications where content freshness is critical, providing a seamless experience without sacrificing performance.

We can also revalidate multiple resources by using revalidateTag putting in the fetch function the tags array

typescript

//revalidateTag refreshes all pages associated with the given tags.
revalidateTag('product-list');

tsx

// app/products/page.ts

export default async function ProductListPage() {
const products = await fetch('https://.../products', {
next: {
revalidate: {
tags: ['product-list'], // Tag for the product list
},
},
})
return (
<div>
{/* product list */}
</div>
);
}

tsx

// app/categories/[categoryId]/page.ts

export default async function CategoryPage({ params }) {
const { categoryId } = params;

const products = await fetch(`https://.../categories/${categoryId}/products`, {
next: {
revalidate: {
tags: ['product-list'],
},
},
})

return (
<div>
{/* render product list based on category */}
</div>
);
}

By leveraging revalidateTag, you can efficiently manage content updates for both the product list and category pages in response to changes in your CMS


By using webhooks, you gain precise control over content updates, ensuring your application delivers the most current information without the overhead of continuous server-side revalidation. This approach offers a balance between performance and flexibility, making it ideal for applications where content changes are event-driven rather than time-sensitive. Choose webhooks when you want to minimize server load and maximize efficiency by only updating content when it truly matters.

Client Components

Client-side rendering (CSR) is particularly beneficial for features that require frequent updates, such as chat applications. By fetching chat messages on the client side, we can create a responsive and interactive experience where users see messages in real time as they are sent and received.

tsx

'use client'; // Ensures this component runs on the client side

import React, { useEffect, useState } from 'react';

function ChatComponent({ chatId }) {
// State to hold the list of chat messages
const [messages, setMessages] = useState([]);
// State to manage the current input message
const [inputMessage, setInputMessage] = useState('');

// Fetch messages when the component mounts or when chatId changes
useEffect(() => {
const fetchMessages = async () => {
try {
const response = await fetch(`https://.../${chatId}/messages`);
if (!response.ok) {
throw new Error('Failed to fetch chat messages');
}
const data = await response.json();
setMessages(data);
} catch (err) {
// setting error
} finally {
//setting loading false
}
};

fetchMessages();

// Set up polling to refresh messages every 5 seconds
const interval = setInterval(fetchMessages, 5000);
return () => clearInterval(interval); // Clean up the interval on component unmount
}, [chatId]);


/** rest of code with
* handling new message input change func,
* message list and chat template
*/
}

export default ChatComponent;

Benefits of Client-Side Fetching for Chat

  • Real-Time Interaction: Users experience live message updates, making the chat application responsive and engaging.
  • Immediate Feedback: The optimistic UI provides a seamless user experience by displaying messages instantly.
  • Efficient Resource Use: By handling updates client-side, the server is freed up to manage other operations, improving overall performance.

This approach leverages client-side capabilities to deliver a dynamic and interactive chat experience, showcasing the power of CSR in modern web applications.

Summary

In this post, we briefly explored data fetching in Next.js, covering both server-side and client-side techniques. We discussed how React Server Components (RSC) and Incremental Static Regeneration (ISR) optimize server-side data fetching, while on-demand revalidation and client-side rendering enhance real-time interactivity and responsiveness in web applications.

Thank you for reading, and we hope you found these insights helpful for building dynamic and efficient applications with Next.js!