Next.js

Data Fetching and Caching

by Tomas Trescak t.trescak@westernsydney.edu.au

Next.js

/

Why Data Fetching?

  • Dynamic Content
  • User Interaction
  • Fresh Information

Next.js

/

Why Caching?!

  • Performance
  • Efficiency
  • Offline Access

Next.js

/

Server-Side Data Fetching

  • Faster Load Times
  • Better SEO
  • Secure Data Handling

Next.js

/

Server-Side Data Fetching

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

Next.js

/

Server-Side Data Fetching

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

type Params = { params: { id: string } }

export default async function Page({ params }: Params) {
  const { id } = await params;
  const post = await db.select().from(posts).where({ id });

  return (
    <main>
      <h1>{post.title}</h1>
      <p>{post.description}</p>
    </main>
  );
}
  
/app/post/[id]/page.tsx

Next.js

/

Client-Side Data Fetching

  • Dynamic Updates
  • User-Specific Data
  • Client-Side Caching
https://tutorialsight.com/next-js-vs-react/

Next.js

/

Client-Side Data Fetching

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

Next.js

/

Tanstack Query

    'use client';

import { useQuery } from '@tanstack/react-query';
import { useParams } from "next/navigation";

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

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

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}
  
/app/post/[id]/page.tsx

Next.js

/

Caching

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

Next.js

/

Cache Invalidation

    import { revalidateTag } from "next/cache";
import { unstable_cache } from "next/cache";

export async function invalidateUserData() {
  await revalidateTag("posts");

  // OR

  await unstable_cache.clear(["posts"]);
}
  
/app/utils/cache.ts

Next.js

/

Reusing Data

    type Post = {}
// ... (getPost function and other code)

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

  return posts.map((post) => ({
    slug: post.slug,
  }));
}
  
/ app/blog/[slug]/page.tsx

Next.js

/

Parallel Fetching

    export default async function ParallelHome() {
  const latestPosts = await getLatestPosts();
  const popularPosts = await getPopularPosts(),
  
  //... render latestPosts and popularPosts...
}

export default async function ParallelHome() {
  const [latestPosts, popularPosts] = await Promise.all([
    getLatestPosts(),
    getPopularPosts(),
  ]);
  
  //... render latestPosts and popularPosts...
}
  
/ app/blog/[slug]/page.tsx

Next.js

/

Preloading

    // 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...
}
  
/ app/blog/[slug]/page.tsx

Next.js

/

Caching and Server-Only

    import { cache } from 'react';
import 'server-only';

export const getPost = cache(async (slug) => {
  const res = await fetch(`https://our-blog-api.com/posts/${slug}`);
  <narration>Fetches the post data.</narration>
  return res.json();
});
  
/utils/blog.ts

Next.js

/

Tainting Information

    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', post);
  experimental_taintUniqueValue(
    'Do not send email', 
    post, 
    post.author.email
  );

  return post;
}
  
/utils/blog.ts

Next.js

/

Key Takeaways

  • Server-side fetching for initial load and SEO.
  • Client-side fetching for dynamic updates.
  • Caching for performance and efficiency.
  • TanStack Query simplifies client-side data fetching.
  • Protect sensitive data with Taint API.