PresentationInteractive, narrated presentation of the section content.

ContentDetailed description of the section content.

1. Introduction or Motivation

Welcome, everyone, to today's lecture on data fetching and caching in Next.js. As we continue developing our blog application, we need to make it dynamic. A static blog isn't very useful, so we need to fetch data from an API or database. This is where data fetching comes in. However, fetching data every time a user visits a page can be slow. That’s where caching comes in. Caching saves a copy of the data, allowing us to quickly serve it to users. Today, we'll learn how to fetch and cache data effectively in Next.js.

2. Lecture: Mastering Data Fetching and Caching

2.1 Fetching Data on the Server

Server-side data fetching is generally preferred in Next.js. It allows us to fetch data before the page is sent to the user, leading to faster load times and better SEO.

Simple Example:

Let's say we have an API endpoint https://api.vercel.app/blog that returns blog posts.

// app/page.tsx
export default async function Page() {
  const res = await fetch('https://api.vercel.app/blog');
  const posts = await res.json();

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

This code fetches data from the API and renders a list of post titles. Because no dynamic APIs are used, the page will be pre-rendered during the build process.

More Complex Example:

Using a database with an ORM like Drizzle:

// app/page.tsx
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>
  );
}

This fetches data from the database using Drizzle. Again, this will be pre-rendered unless dynamic APIs are used.

2.2 Fetching Data on the Client

Sometimes, client-side fetching is necessary, like for dynamic updates. See the use of useEffect to fetch and store data in the state.

Example:

// app/page.tsx
'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>
  );
}

The 'use client' directive is essential here.

While using fetch works, it quite bare-bones. There are existing solutions such as @tanstack/query that optimise client side data fetching:

Example using TanStack Query for Client-side Fetching:

// app/page.tsx
'use client';

import { useQuery } from '@tanstack/react-query';

export function Posts() {
  const { data: posts, isLoading } = useQuery({
    queryKey: ['posts'],
    queryFn: async () => {
      const res = await fetch('https://api.vercel.app/blog');
      return res.json();
    },
  });

  if (isLoading) return <div>Loading...</div>;

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

In this example, we're using the TanStack Query library to fetch and cache data on the client-side. Here's how it works:

  • useQuery is a hook provided by TanStack Query that manages fetching and caching data.
  • queryKey is a unique key for the query. TanStack Query uses this key to cache the data. In this case, we're using ['posts'].
  • queryFn is an asynchronous function that fetches the data. In this case, it's fetching data from the API endpoint.
  • useQuery returns an object with various properties, including data (the fetched data) and isLoading (a boolean indicating whether the data is currently being fetched).

TanStack Query provides many benefits, including automatic caching, background updates, and request deduplication. It's a powerful tool for managing data fetching in your Next.js applications.

2.3 Caching Data

Caching is like storing a backup of your data. Instead of fetching it repeatedly, you retrieve it from the cache, which is much faster. Next.js provides various ways to cache data, and we'll explore a couple of them.

Example using <strong>unstable_cache</strong>:

// app/page.tsx
import { unstable_cache } from 'next/cache';
import { db, posts } from '@/lib/db';

const getPosts = unstable_cache(
  async () => {
    return 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>
  );
}

In this example, we're using the unstable_cache function to cache the result of our database query. Let's break down what's happening:

  • unstable_cache is a Next.js API that allows us to cache the output of a function.
  • The first argument to unstable_cache is the function we want to cache. In this case, it's an asynchronous function that fetches all posts from our database using Drizzle ORM.
  • The second argument is an array of dependencies. These are values that, if changed, will cause the cache to be invalidated and the function to be re-executed. In this case, we're using ['posts'], which means the cache will be invalidated if the data in the posts table changes.
  • The third argument is an options object. Here, we're setting revalidate to 3600, which means the cache will be revalidated every hour. We're also setting tags to ['posts'], which allows us to invalidate this cache using Incremental Static Regeneration (ISR) if needed.

2.4 Reusing Data in the Blog App

Remember how we built the individual blog post pages? We fetched the post data using its unique ID. Now, imagine we also want to show a list of recent posts on the homepage and include the post title in the page's metadata for better SEO. Instead of fetching the same post data multiple times, we can reuse the data fetching function.

// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';

interface Post {
  slug: string;
  title: string;
  content: string;
}

async function getPost(slug: string): Promise<Post> {
  const res = await fetch(`https://our-blog-api.com/posts/${slug}`, {
    // Next.js will cache this request
    cache: 'force-cache',
  });
  const post: Post = await res.json();
  if (!post) {
    notFound();
  }
  return post;
}

export async function generateStaticParams() {
  const posts: Post = await fetch('https://our-blog-api.com/posts', {
    // Next.js will cache this request
    cache: 'force-cache',
  }).then((res) => res.json());

  return posts.map((post) => ({
    slug: post.slug,
  }));
}

export async function generateMetadata({ params }: { params: { slug: string } }) {
  const post: Post = await getPost(params.slug);

  return {
    title: post.title,
  };
}

export default async function Page({ params }: { params: { slug: string } }) {
  const post: Post = await getPost(params.slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

Here, the getPost function fetches a single post using its slug. We use cache: 'force-cache' to ensure Next.js caches the response. This function is reused in generateStaticParams to fetch all posts for dynamic route generation, in generateMetadata to get the post title, and in the page component to display the post content.

2.5 Parallel vs. Sequential Fetching in the Blog

Let's say on our blog's homepage, we want to display a list of popular posts alongside the latest posts. We could fetch these sequentially, but fetching in parallel is much faster.

Sequential:

// app/page.tsx
async function getLatestPosts() {
  const res = await fetch('https://our-blog-api.com/posts?order=latest');
  return res.json();
}

async function getPopularPosts() {
  const res = await fetch('https://our-blog-api.com/posts?order=popular');
  return res.json();
}

export default async function Home() {
  const latestPosts = await getLatestPosts();
  const popularPosts = await getPopularPosts();

  //... render latestPosts and popularPosts...
}

This fetches latest posts first, then popular posts, potentially causing delays.

Parallel:

// app/page.tsx
async function getLatestPosts() {
  const res = await fetch('https://our-blog-api.com/posts?order=latest');
  return res.json();
}

async function getPopularPosts() {
  const res = await fetch('https://our-blog-api.com/posts?order=popular');
  return res.json();
}

export default async function Home() {
  const [latestPosts, popularPosts] = await Promise.all([
    getLatestPosts(),
    getPopularPosts(),
  ]);

  //... render latestPosts and popularPosts...
}

By using Promise.all, both fetches happen simultaneously, making the page load faster.

2.6 Preloading for a Smoother Blog Experience

Imagine a user browsing our blog's homepage. They click on a post that catches their eye. With preloading, we can start fetching that post's data as soon as they hover over the link, making the transition smoother.

// components/PostLink.tsx
import { preload } from './Post';

export default function PostLink({ post }) {
  return (
    <Link href={`/blog/${post.slug}`} onMouseEnter={() => preload(post.slug)}>
      {post.title}
    </Link>
  );
}

// components/Post.tsx
import { cache } from 'react';

export const preload = (slug) => {
  void getPost(slug);
};

const getPost = cache(async (slug) => {
  const res = await fetch(`https://our-blog-api.com/posts/${slug}`);
  return res.json();
});

export default async function Post({ slug }) {
  const post = await getPost(slug);

  //... render post...
}

When the user hovers over the PostLink, preload is called, initiating the data fetch for the Post component.

2.7 Caching and Server-Only for Blog Posts

We can combine React's cache and the server-only package to optimise fetching individual blog posts.

// utils/blog.ts
import { cache } from 'react';
import 'server-only';

export const preload = (slug) => {
  void getPost(slug);
};

export const getPost = cache(async (slug) => {
  const res = await fetch(`https://our-blog-api.com/posts/${slug}`);
  return res.json();
});

The cache function memoizes getPost, and server-only ensures the data fetching happens only on the server.

2.8 Protecting Author Information

Let's say our blog API also returns sensitive author information, like their email address. We can use taint APIs to prevent this from being sent to the client. 

First, you need to enable taint API in next.config.json:

module.exports = {
  experimental: {
    taint: true,
  },
}

Here is the example:

// utils/blog.ts
import {
  experimental_taintObjectReference,
  experimental_taintUniqueValue,
} from 'react';

export async function getPost(slug) {
  const res = await fetch(`https://our-blog-api.com/posts/${slug}`);
  const post = await res.json();

  experimental_taintObjectReference('Do not send the entire post object to the client', post);
  experimental_taintUniqueValue('Do not send the author email to the client', post, post.author.email);

  return post;
}

This prevents the entire post object and the author.email specifically from reaching the client.

By applying these data fetching and caching techniques to our blog application, we can make it faster, more efficient, and more secure.

No hints available.

Discuss with OthersAsk questions, share your thoughts, and discuss with other learners.

Loading discussion ...