AgentSkillsCN

Data Fetching

数据获取

SKILL.md

Next.js Data Fetching & Caching


name: data-fetching description: "Next.js data fetching strategies and caching patterns. Covers Server Component fetching, React cache(), Next.js caching layers, revalidation strategies (time-based, on-demand, tag-based), ISR, SWR and TanStack Query for client-side, parallel vs sequential fetching, and Streaming with Suspense." license: MIT metadata: author: Balazs Barta version: "0.1.0"

Overview

Effective data fetching in Next.js requires understanding multiple caching layers and when to use server-side vs client-side fetching. This skill covers:

  • Server Component data fetching patterns
  • Next.js 4-layer caching architecture
  • Revalidation strategies (time-based, on-demand, tag-based)
  • Parallel and sequential data fetching
  • Client-side fetching with SWR and TanStack Query
  • Streaming with Suspense boundaries

Quick Lookup: Next.js Documentation

Always check the Next.js llms.txt first for:

  • Data Fetching API reference
  • Caching and Revalidation docs
  • Route Handlers and Server Components

Server Component Data Fetching

Server Components allow direct database or API queries without exposing credentials:

Basic Fetch

typescript
// app/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();
  return posts;
}

export default async function Page() {
  const posts = await getPosts();

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>{post.title}</article>
      ))}
    </div>
  );
}

Database Query

typescript
// app/page.tsx
import { db } from '@/lib/db';

async function getPosts() {
  // Direct database query - credentials never exposed to client
  return await db.posts.findAll();
}

export default async function Page() {
  const posts = await getPosts();

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>{post.title}</article>
      ))}
    </div>
  );
}

Next.js Caching Layers

Next.js uses 4 independent caching layers:

1. Request Memoization

React automatically deduplicates fetch() calls with the same URL during a single render pass:

typescript
// app/page.tsx
async function getUser() {
  const res = await fetch('https://api.example.com/user/1');
  return res.json();
}

async function getUserPosts() {
  // Even though fetch is called in two places, only one request is made
  const user = await getUser();
  return await fetch(`https://api.example.com/user/${user.id}/posts`);
}

export default async function Page() {
  // These two calls both fetch the user, but result in only 1 HTTP request
  const user = await getUser();
  const posts = await getUserPosts();

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

2. Data Cache (Persistent)

The Data Cache stores responses across deployments. By default, all fetch() calls are cached:

typescript
// This response is cached indefinitely
async function getPosts() {
  const res = await fetch('https://api.example.com/posts');
  return res.json();
}

Opt out of Data Cache:

typescript
// Skip caching for this fetch
const res = await fetch('https://api.example.com/posts', {
  cache: 'no-store' // Don't cache this request
});

3. Full Route Cache

Next.js pre-renders entire routes at build time and serves cached HTML/RSC:

typescript
// app/blog/page.tsx
// This entire page is cached at build time
export default async function Page() {
  const posts = await getPosts();
  return <div>{/* ... */}</div>;
}

Disable Full Route Cache:

typescript
// This page is never cached
export const dynamic = 'force-dynamic';

export default async function Page() {
  const posts = await getPosts();
  return <div>{/* ... */}</div>;
}

4. Router Cache (Browser)

Client-side cache of React Server Component payloads:

typescript
// Automatically cached in browser for 30 seconds
export default async function Page() {
  const posts = await getPosts();
  return <div>{/* ... */}</div>;
}

Disable Router Cache:

typescript
export const revalidate = 0; // Don't cache in browser

Revalidation Strategies

Time-Based Revalidation

Revalidate data after a specific time interval:

typescript
// app/blog/page.tsx
// Revalidate this page every 60 seconds
export const revalidate = 60;

async function getPosts() {
  const res = await fetch('https://api.example.com/posts');
  return res.json();
}

export default async function Page() {
  const posts = await getPosts();
  return <div>{/* ... */}</div>;
}

Also works per-fetch:

typescript
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 } // Revalidate every 60 seconds
  });
  return res.json();
}

On-Demand Revalidation (Path-Based)

Manually trigger revalidation for a specific path:

typescript
// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  const post = await db.posts.create({
    title: formData.get('title') as string,
  });

  // Revalidate the /blog page
  revalidatePath('/blog');

  return post;
}

On-Demand Revalidation (Tag-Based)

Tag fetches and revalidate by tag:

typescript
// app/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { tags: ['posts'] } // Tag this fetch
  });
  return res.json();
}

Revalidate by tag:

typescript
// app/actions.ts
'use server';

import { revalidateTag } from 'next/cache';

export async function createPost(formData: FormData) {
  await db.posts.create({
    title: formData.get('title') as string,
  });

  revalidateTag('posts'); // Revalidate all fetches tagged 'posts'

  return { success: true };
}

Incremental Static Regeneration (ISR)

Generate static pages on demand and revalidate them:

typescript
// app/blog/[slug]/page.tsx
export const revalidate = 3600; // Revalidate every hour

export async function generateStaticParams() {
  // Generate pages for these slugs at build time
  const posts = await db.posts.findAll();
  return posts.map(post => ({ slug: post.slug }));
}

export async function generateMetadata({ params }: any) {
  const post = await db.posts.findBySlug(params.slug);
  return { title: post.title };
}

export default async function Page({ params }: any) {
  const post = await db.posts.findBySlug(params.slug);

  return <article>{post.content}</article>;
}

Parallel Data Fetching

Fetch multiple resources concurrently:

typescript
// app/dashboard/page.tsx
async function getUser() {
  const res = await fetch('https://api.example.com/user');
  return res.json();
}

async function getPosts() {
  const res = await fetch('https://api.example.com/posts');
  return res.json();
}

async function getComments() {
  const res = await fetch('https://api.example.com/comments');
  return res.json();
}

export default async function Dashboard() {
  // These run in parallel, not sequentially
  const [user, posts, comments] = await Promise.all([
    getUser(),
    getPosts(),
    getComments(),
  ]);

  return <div>{/* ... */}</div>;
}

Sequential Data Fetching

When you need data from one request to make the next:

typescript
// app/user/[id]/page.tsx
async function getUser(userId: string) {
  const res = await fetch(`https://api.example.com/user/${userId}`);
  return res.json();
}

async function getUserPosts(userId: string) {
  const res = await fetch(`https://api.example.com/user/${userId}/posts`);
  return res.json();
}

export default async function UserPage({ params }: any) {
  // First request
  const user = await getUser(params.id);

  // Second request depends on first
  const posts = await getUserPosts(user.id);

  return <div>{/* ... */}</div>;
}

Streaming with Suspense

Stream content to the client progressively while data is being fetched:

typescript
// app/blog/page.tsx
import { Suspense } from 'react';

async function PostsList() {
  // This might be slow
  const posts = await fetchPostsSlowly();

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>{post.title}</article>
      ))}
    </div>
  );
}

function PostsLoadingFallback() {
  return <div>Loading posts...</div>;
}

export default function Page() {
  return (
    <div>
      <h1>Blog</h1>
      <Suspense fallback={<PostsLoadingFallback />}>
        <PostsList />
      </Suspense>
    </div>
  );
}

Nested Suspense boundaries:

typescript
// app/dashboard/page.tsx
import { Suspense } from 'react';

function UserSkeleton() {
  return <div className="skeleton">Loading user...</div>;
}

function PostsSkeleton() {
  return <div className="skeleton">Loading posts...</div>;
}

async function UserCard() {
  const user = await getUser();
  return <div>{user.name}</div>;
}

async function PostsList() {
  const posts = await getPosts();
  return <div>{posts.length} posts</div>;
}

export default function Dashboard() {
  return (
    <div>
      <Suspense fallback={<UserSkeleton />}>
        <UserCard />
      </Suspense>

      <Suspense fallback={<PostsSkeleton />}>
        <PostsList />
      </Suspense>
    </div>
  );
}

Client-Side Data Fetching

Use client-side fetching for:

  • Real-time data that updates frequently
  • User-specific data
  • Data based on user interactions
typescript
'use client';

import { useEffect, useState } from 'react';

export default function Posts() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch('/api/posts')
      .then(res => res.json())
      .then(data => {
        setPosts(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>{post.title}</article>
      ))}
    </div>
  );
}

SWR (stale-while-revalidate)

Lightweight client-side fetching library with automatic revalidation:

typescript
'use client';

import useSWR from 'swr';

const fetcher = (url: string) => fetch(url).then(res => res.json());

export default function Posts() {
  const { data: posts, error, isLoading } = useSWR('/api/posts', fetcher);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>{post.title}</article>
      ))}
    </div>
  );
}

Key features:

  • Automatic revalidation (by default every 2 seconds while focused)
  • Request deduplication
  • Pagination support
  • Optimistic mutations

TanStack Query (React Query)

Powerful client-side data fetching and caching library:

typescript
'use client';

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

function usePosts() {
  return useQuery({
    queryKey: ['posts'],
    queryFn: async () => {
      const res = await fetch('/api/posts');
      return res.json();
    },
    staleTime: 60000, // 1 minute
  });
}

export default function Posts() {
  const { data: posts, isLoading, error } = usePosts();

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>{post.title}</article>
      ))}
    </div>
  );
}

With mutations:

typescript
'use client';

import { useMutation, useQueryClient } from '@tanstack/react-query';

export default function CreatePostForm() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: async (newPost) => {
      const res = await fetch('/api/posts', {
        method: 'POST',
        body: JSON.stringify(newPost),
      });
      return res.json();
    },
    onSuccess: () => {
      // Revalidate posts query
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
  });

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      mutation.mutate({ title: 'New Post' });
    }}>
      <button disabled={mutation.isPending}>
        {mutation.isPending ? 'Creating...' : 'Create Post'}
      </button>
    </form>
  );
}

SWR vs TanStack Query

FeatureSWRTanStack Query
Bundle Size~13kb~32kb
ComplexitySimplerMore powerful
MutationsBasicAdvanced with invalidation
CachingSimplerGranular control
PaginationBuilt-in helpersManual setup
Best ForSimple appsComplex enterprise apps

Common Data Fetching Mistakes

Waterfall Requests (Slow)

typescript
// ❌ BAD: Sequential requests
export default async function Page() {
  const user = await getUser(); // Wait for this
  const posts = await getPosts(user.id); // Then do this
  const comments = await getComments(); // Then this

  return <div>{/* ... */}</div>;
}

Parallel Requests (Fast)

typescript
// ✅ GOOD: Fetch in parallel
export default async function Page() {
  const [user, posts, comments] = await Promise.all([
    getUser(),
    getPosts(),
    getComments(),
  ]);

  return <div>{/* ... */}</div>;
}

Over-Fetching Data

typescript
// ❌ BAD: Fetch everything
const allUsers = await db.users.findAll(); // 1000s of users

// ✅ GOOD: Fetch only what you need
const users = await db.users.findAll({ limit: 10, offset: 0 });

Wrong Caching Strategy

typescript
// ❌ BAD: Caching dynamic data
export const revalidate = 3600;

async function getCurrentUser() {
  return await db.users.findById(currentUserId); // Changes per request!
}
typescript
// ✅ GOOD: Don't cache user-specific data
export const dynamic = 'force-dynamic';

async function getCurrentUser() {
  return await db.users.findById(currentUserId);
}

Resources

For detailed patterns and examples, see:

  • /skills/data-fetching/references/caching-strategies.md - Deep dive into caching layers
  • /skills/data-fetching/references/client-fetching.md - Client-side fetching patterns with SWR and TanStack Query