Fetching Data
by Tomas Trescak· Databases

0 / 1930 XP

Presentation Transcript
If you prefer text to video, check out the transcript of the presentation above

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.

Description
All the extra information about this section

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

Discuss with Maggie
Use the power of generative AI to interact with course content

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.

Discuss with Others
Ask questions, share your thoughts, and discuss with other learners

Join the discussion to ask questions, share your thoughts, and discuss with other learners
Setup
React Fundamentals
10 points
Next.js
10 points
Advanced React
Databases
10 points
React Hooks
Authentication and Authorisation
10 points
APIs
CI/CD and DevOps
Testing React
Advanced Topics