Chuyển đến nội dung chính

Lesson 10: Data Fetching & Caching

Server-side data fetching, fetch API caching. Static vs dynamic rendering, ISR. Parallel data fetching, waterfall prevention. unstable_cache, revalidation.

💻 Programming — Lesson 10 Lesson 10: Data Fetching & Caching

React & Next.js: From Basics to Advanced

Part 3: Next.js Fundamentals

xdev.asia

1. Server-side Data Fetching

// Async Server Component — fetch directly
export default async function PostsPage() {
  const posts = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 }, // ISR: revalidate every hour
  }).then(res => res.json());

  return (
    <div>
      {posts.map((post: Post) => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  );
}

2. Caching Strategies

// Static (cached forever until redeployed)
const data = await fetch('https://api.example.com/data', {
  cache: 'force-cache', // Default behavior
});

// Dynamic (no cache, fresh every request)
const data = await fetch('https://api.example.com/data', {
  cache: 'no-store',
});

// ISR (revalidate after N seconds)
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 60 }, // Revalidate every 60 seconds
});

// Tag-based revalidation
const data = await fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] },
});

// Revalidate by tag (from Server Action or Route Handler)
import { revalidateTag, revalidatePath } from 'next/cache';
revalidateTag('posts');       // Invalidate all fetches with 'posts' tag
revalidatePath('/blog');      // Invalidate specific path

3. Parallel Data Fetching

// ❌ Waterfall — sequential fetching
export default async function Dashboard() {
  const user = await fetchUser();        // 500ms
  const orders = await fetchOrders();    // 500ms
  const analytics = await fetchAnalytics(); // 500ms
  // Total: 1500ms 😱
}

// ✅ Parallel — concurrent fetching
export default async function Dashboard() {
  const [user, orders, analytics] = await Promise.all([
    fetchUser(),        // 500ms
    fetchOrders(),      // 500ms ← concurrent
    fetchAnalytics(),   // 500ms ← concurrent
  ]);
  // Total: 500ms 🚀
}

// ✅ Streaming — each section loads independently
export default function Dashboard() {
  return (
    <div>
      <Suspense fallback={<Skeleton />}>
        <UserInfo />
      </Suspense>
      <Suspense fallback={<Skeleton />}>
        <OrderList />
      </Suspense>
      <Suspense fallback={<Skeleton />}>
        <Analytics />
      </Suspense>
    </div>
  );
}

4. Direct Database Access

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

// Truy vấn database trực tiếp trong Server Component
async function UserList() {
  const users = await db.user.findMany({
    where: { isActive: true },
    orderBy: { createdAt: 'desc' },
    take: 10,
  });

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

5. unstable_cache

import { unstable_cache } from 'next/cache';

const getCachedUser = unstable_cache(
  async (userId: string) => {
    return db.user.findUnique({ where: { id: userId } });
  },
  ['user'], // Cache key prefix
  {
    revalidate: 3600, // 1 hour
    tags: ['user'],   // Tag for manual revalidation
  }
);

// Usage in Server Component
export default async function Profile({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const user = await getCachedUser(id);

  return <div>{user?.name}</div>;
}

6. Static vs Dynamic Rendering

RenderingWhenCache
StaticNo dynamic data, no cookies/headersBuild time, CDN
Dynamiccookies(), headers(), searchParamsRequest time
ISRStatic + revalidateBuild time + periodic refresh
// Force dynamic rendering
export const dynamic = 'force-dynamic';
// Or: export const revalidate = 0;

// Force static
export const dynamic = 'force-static';

Next article: Server Actions & Mutations — "use server", form actions, optimistic updates.