Server-Side Data Fetching in Next.js: Key Concepts and Best Practices

To Nha Notes | Jan. 7, 2025, 10:28 p.m.

Next.js provides a flexible and efficient approach to data fetching, allowing you to handle server-side and client-side needs seamlessly. Below are technical notes summarizing the key aspects:


1. Server-Side Data Fetching with fetch

Example:

export default async function Page() {
  const data = await fetch('https://api.vercel.app/blog');
  const posts = await data.json();
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

Key Points:

  • The fetch API retrieves data during the server-side rendering process.
  • The response is not cached by default.
  • Pages are prerendered during the build if no dynamic APIs are used. Data updates can leverage Incremental Static Regeneration (ISR).
  • To force dynamic rendering, add:
    export const dynamic = 'force-dynamic';
    
  • Using cookies, headers, or search parameters makes the page dynamic automatically.

2. Server-Side Data Fetching with an ORM or Database

Example:

import { db, posts } from '@/lib/db';

export default async function Page() {
  const allPosts = await db.select().from(posts);
  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

Key Points:

  • Fetch data from a database using ORM queries.
  • The response is not cached by default, but caching can be added (see below).
  • Similarly to fetch, use dynamic = 'force-dynamic' if needed.

3. Client-Side Data Fetching

Client-side fetching is less common but useful in scenarios requiring dynamic updates after the page loads.

Example with useEffect:

'use client';

import { useState, useEffect } from 'react';

export function Posts() {
  const [posts, setPosts] = useState(null);

  useEffect(() => {
    async function fetchPosts() {
      const res = await fetch('https://api.vercel.app/blog');
      const data = await res.json();
      setPosts(data);
    }
    fetchPosts();
  }, []);

  if (!posts) return <div>Loading...</div>;

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

Recommended Tools:

  • Use libraries like SWR or React Query for advanced client-side fetching and caching.

4. Caching with unstable_cache

You can cache server-side responses to improve performance and enable ISR.

Example:

import { unstable_cache } from 'next/cache';
import { db, posts } from '@/lib/db';

const getPosts = unstable_cache(
  async () => await db.select().from(posts),
  ['posts'],
  { revalidate: 3600, tags: ['posts'] }
);

export default async function Page() {
  const allPosts = await getPosts();
  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

Key Points:

  • Cache results for a specified duration (revalidate).
  • Use tags to invalidate the cache as needed.

5. Reusing Data Across Functions

Next.js APIs like generateMetadata and generateStaticParams allow you to reuse fetched data.

6: Migrating Data Fetching Methods

The pages directory uses getServerSideProps and getStaticProps to fetch data for pages. Inside the app directory, these previous data fetching functions are replaced with a simpler API built on top of fetch() and async React Server Components.

export default async function Page() {
  // This request should be cached until manually invalidated.
  // Similar to `getStaticProps`.
  // `force-cache` is the default and can be omitted.
  const staticData = await fetch(`https://...`, { cache: 'force-cache' })
 
  // This request should be refetched on every request.
  // Similar to `getServerSideProps`.
  const dynamicData = await fetch(`https://...`, { cache: 'no-store' })
 
  // This request should be cached with a lifetime of 10 seconds.
  // Similar to `getStaticProps` with the `revalidate` option.
  const revalidatedData = await fetch(`https://...`, {
    next: { revalidate: 10 },
  })
 
  return <div>...</div>
}

In the app directory, data fetching with fetch() will default to cache: 'force-cache', which will cache the request data until manually invalidated. This is similar to getStaticProps in the pages directory.

// `app` directory
 
// This function can be named anything
async function getProjects() {
  const res = await fetch(`https://...`)
  const projects = await res.json()
 
  return projects
}
 
export default async function Index() {
  const projects = await getProjects()
 
  return projects.map((project) => <div>{project.name}</div>)
}

 


References

For further reading and examples:

By understanding these approaches, you can optimize your data fetching strategy in Next.js, balancing performance and flexibility.