Slide 1
-----------
Part 1: Welcome, everyone, to today's lecture on data fetching and caching in Next.js. We'll be building on our blog application example to explore these crucial concepts.
Slide 2
-----------
Part 1: Why do we need to fetch data?
Part 2: Our blog needs to be dynamic, pulling content from a database or API, not just static HTML.
Part 3: We want to enable features like comments, search, and personalized recommendations, all requiring data fetching.
Part 4: News, updates, and new blog posts need to be fetched and displayed regularly.
Slide 3
-----------
Part 1: One of the most impactful features of NextJS is caching, which allows you to deliver blazing-fast applications. So, why do we cache?
Part 2: Fetching data every time is slow. Caching stores data for quick retrieval, improving user experience.
Part 3: Caching reduces load on our servers and databases, saving resources.
Part 4: Caching can allow users to access content even when offline.
Slide 4
-----------
Part 1: Let's briefly discuss the difference between fetching data on a server and a client. Both have their pros and cons.
Part 2: Think of server-side fetching as the restaurant's kitchen preparing the dish before it's served to the customer. Everything is ready when the customer asks for it. As a result, the data is fetched before the page reaches the user, leading to a faster initial load.
Part 3: Search engines can easily crawl pre-rendered content.
Part 4: Sensitive data can remain on the server.
Slide 5
-----------
Part 1: This simple example fetches blog post titles and displays them in a list. Because no dynamic APIs are used, the page will be pre-rendered during the build process.
Part 2: Here, we simply fetch data from the specified API endpoint. Note the asynchronous call.
Part 3: Then, we just parse the retrieved data into JSON and use it in the component.
Slide 6
-----------
Part 1: This example uses a database and an ORM called Drizzle; we will cover ORM models that soon.
Part 2: Here, we fetch the data ...
Part 3: ... and display them in a post. Will this be rendered dynamically or statically? If you guessed dynamically, then you are right! As an exercise, think how you can turn it into a statically rendered route!
Slide 7
-----------
Part 1: Let's talk about client-side fetching. Client-side fetching is like the Tepanayaki restaurant taking your specific order and preparing it in front of you after you've sat at your table.
Part 2: The browser fetches data after the page loads, which is useful for interactive elements.
Part 3: Consequently, the client can fetch data based on user actions or input.
Part 4: And we can cache data client side, so repetitive calls to server are not necessary.
Slide 8
-----------
Part 1: This example uses useEffect to fetch data after the component mounts, storing them in the component state. The 'use client' directive is crucial here.
Part 2: In the useEffect, the component fetches data from the API, parses the response as JSON and updates the state with the fetched data.
Part 3: If the data is not available, the component assumes that it is still loading and displays the message to the user.
Part 4: Once the data has been retrieved, the component displays them.
Slide 9
-----------
Part 1: This example uses TanStack Query, a powerful library for managing client-side data fetching. It handles caching, background updates, and more.
Part 2: It also uses the useParams hook from NextJS that allows you to retrieve page params in the client component!
Part 3: The useQuery hook from tanstack needs a unique id, under which it will store the cached data.
Part 4: The query function is an asynchronous function that is responsible for fetching data to the client.
Part 5: The isLoading parameter from Tanstack Query specifies if data is loaded from the server.
Part 6: Once the data has been loaded, we can show them. Please note how we renamed the data parameter to posts in the return value of useQuery.
Slide 10
-----------
Part 1: You can further optimise the speed of your application by caching your data sources. This example demonstrates caching with unstable_cache. The function's result is cached, and the cache is revalidated every hour.
Part 2: This is the function to be cached, fetching posts from the database.
Part 3: You must also provide an array of dependencies. If these change, the cache is invalidated, similar to React hooks.
Part 4: You can specify how often to revalidate the data, in this case, every hour. You can also provide the tag which further controls invalidation.
Part 5: Once you set up a cached function, you can simply call it to retrieve your data.
Slide 11
-----------
Part 1: If you want to manually invalidate the cache, for example after data update you have two options.
Part 2: You can invalidate data based on the tag you provided to the cache function ...
Part 3: ... or by the cache key.
Slide 12
-----------
Part 1: NextJS comes with further optimisations for caching data requests.
Part 2: For example, you can cache the fetch requests by providing the force-cache value to the cache parameter. If you try to fetch the data from the same url in a different component, the data will be reused.
Slide 13
-----------
Part 1: When fetching data sequentially, your requests execute in waterfall method, increasing the load time
Part 2: Instead, you can use the Promise all function to execute the data fetching in parallel, and requests will not wait for each other.
Slide 14
-----------
Part 1: You can further optimise user experience by preloading data
Part 2: For example, this link component preloads the post data when the mouse hovers over the link, so that when user clicks on the link the data will be instantly available.
Part 3: The preload function uses cache to store the data ...
Part 4: ... so when the page loads, the data will be available in the cache for instant access.
Slide 15
-----------
Part 1: Combining cache and server-only ensures data fetching happens only on the server and is memoized for efficiency.
Slide 16
-----------
Part 1: Taint APIs prevent sensitive data, like author emails, from being exposed on the client-side, enhancing security.
Part 2: taint Object Reference command marks the entire post object as tainted, preventing it from being sent to the client.
Part 3: taint Unique Value command taints the author's email, preventing it from being sent to the client.
Slide 17
-----------
Part 1: Remember, choosing the right data fetching and caching strategy is crucial for building performant and secure Next.js applications.
Part 2: Prioritize server-side rendering for optimal performance and search engine visibility.
Part 3: Use client-side fetching for interactive elements and user-specific data.
Part 4: Leverage caching mechanisms to reduce server load and improve user experience.
Part 5: Consider using TanStack Query for robust client-side data management.
Part 6: Employ Taint API to safeguard sensitive information.
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.
Maggie is a generative AI that can help you understand the course content better. You can ask her questions about the lecture, and she will try to answer them. You can also see the questions asked by other students and her responses.
Join the discussion to ask questions, share your thoughts, and discuss with other learners