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:
useQueryis a hook provided by TanStack Query that manages fetching and caching data.queryKeyis a unique key for the query. TanStack Query uses this key to cache the data. In this case, we're using['posts'].queryFnis an asynchronous function that fetches the data. In this case, it's fetching data from the API endpoint.useQueryreturns an object with various properties, includingdata(the fetched data) andisLoading(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_cacheis a Next.js API that allows us to cache the output of a function.- The first argument to
unstable_cacheis 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 thepoststable changes. - The third argument is an options object. Here, we're setting
revalidateto 3600, which means the cache will be revalidated every hour. We're also settingtagsto['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.