Menu

React & Next.js Topic

Next.js Data Fetching & Caching

Next.js Data Fetching & Caching refers to how Next.js retrieves data (from APIs, databases, etc.) and how it stores/reuses that data to make apps faster, scalable, and more efficient.
It’s a core feature that separates Next.js from plain React.


Where Data Fetching Happens

Next.js can fetch data in two places:

Server Components (default in App Router)

  • Runs on the server

  • Data is never exposed to the browser

  • Great for secure data (DB calls, secrets)

  • Better performance (no client JS required)

Client Components

  • Runs in the browser

  • Used for interactive features (forms, buttons, inputs)

  • Typically fetches data using useEffect or libraries like SWR/React Query

Rule of thumb:
Use server components first, client components only when necessary.

1. Data Fetching Methods

Static Site Generation (SSG)

jsx

// pages/products/[id].js
export default function Product({ product }) {
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}

// getStaticPaths - defines which paths to pre-render
export async function getStaticPaths() {
  // Fetch product IDs from API
  const products = await fetch('https://api.example.com/products').then(res => res.json());
  
  const paths = products.map(product => ({
    params: { id: product.id.toString() }
  }));

  return {
    paths,
    fallback: false // or true or 'blocking'
  };
}

// getStaticProps - fetches data at build time
export async function getStaticProps({ params }) {
  const product = await fetch(`https://api.example.com/products/${params.id}`).then(res => res.json());

  return {
    props: {
      product
    },
    // ISR: Revalidate every 60 seconds
    revalidate: 60
  };
}

Server-Side Rendering (SSR)

jsx

// pages/profile.js
export default function Profile({ user, timestamp }) {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>Rendered at: {timestamp}</p>
    </div>
  );
}

// getServerSideProps - runs on every request
export async function getServerSideProps(context) {
  const { req, res, params, query } = context;
  
  // Access cookies, headers, etc.
  const token = req.cookies.token;
  
  // Fetch user-specific data
  const user = await fetch('https://api.example.com/user', {
    headers: {
      Authorization: `Bearer ${token}`
    }
  }).then(res => res.json());

  return {
    props: {
      user,
      timestamp: new Date().toISOString()
    }
  };
}

Client-Side Fetching

jsx

import { useState, useEffect } from 'react';
import useSWR from 'swr';

// Using native fetch
function ClientSideComponent() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchData() {
      try {
        const response = await fetch('/api/users');
        const users = await response.json();
        setData(users);
      } catch (error) {
        console.error('Failed to fetch:', error);
      } finally {
        setLoading(false);
      }
    }

    fetchData();
  }, []);

  if (loading) return <div>Loading...</div>;
  return <div>{/* Render data */}</div>;
}

// Using SWR for better caching and revalidation
function SWRComponent() {
  const { data, error, isLoading, mutate } = useSWR(
    '/api/users',
    (url) => fetch(url).then(res => res.json()),
    {
      revalidateOnFocus: true,
      revalidateOnReconnect: true,
      refreshInterval: 30000, // 30 seconds
    }
  );

  if (error) return <div>Failed to load</div>;
  if (isLoading) return <div>Loading...</div>;
  
  return (
    <div>
      {data.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
      <button onClick={() => mutate()}>Refresh</button>
    </div>
  );
}

2. Fetch with Caching Options

Native Fetch with Next.js Caching

jsx

// lib/api.js
export async function fetchWithCache(url, options = {}) {
  const { cache = 'force-cache', revalidate, tags } = options;
  
  const response = await fetch(url, {
    ...options,
    next: {
      revalidate, // Time in seconds
      tags, // Array of cache tags for on-demand revalidation
    },
    cache, // 'force-cache' | 'no-store' | 'no-cache' | 'reload' | 'only-if-cached'
  });

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return response.json();
}

// Usage in components
export async function getStaticProps() {
  // Cache for 1 hour (3600 seconds)
  const products = await fetchWithCache('https://api.example.com/products', {
    revalidate: 3600,
    tags: ['products']
  });

  // Don't cache - fresh data on every request
  const userSession = await fetchWithCache('https://api.example.com/session', {
    cache: 'no-store'
  });

  return {
    props: {
      products,
      userSession
    }
  };
}

Cache Strategies by Use Case

jsx

// pages/blog/[slug].js - SSG with ISR
export async function getStaticProps({ params }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
    next: { revalidate: 3600 } // Revalidate every hour
  }).then(res => res.json());

  return { props: { post } };
}

// pages/dashboard.js - SSR with no cache
export async function getServerSideProps() {
  const sensitiveData = await fetch('https://api.example.com/sensitive-data', {
    cache: 'no-store' // Never cache
  }).then(res => res.json());

  return { props: { sensitiveData } };
}

// pages/analytics.js - Client-side with SWR
function AnalyticsPage() {
  const { data } = useSWR('/api/analytics', null, {
    refreshInterval: 5000, // Refresh every 5 seconds
    dedupingInterval: 2000, // Dedupe requests within 2 seconds
  });

  return <div>{/* Real-time analytics */}</div>;
}

Advanced Caching Patterns

jsx

// lib/cache.js
export class DataCache {
  static async get(key, fetcher, options = {}) {
    const { revalidate = 3600, tags = [] } = options;
    
    try {
      const data = await fetch(`https://api.example.com/${key}`, {
        next: { revalidate, tags }
      }).then(res => res.json());
      
      return data;
    } catch (error) {
      // Fallback to stale data or error handling
      console.error('Cache fetch failed:', error);
      throw error;
    }
  }
}

// pages/categories/[category].js
export async function getStaticProps({ params }) {
  const [products, filters] = await Promise.all([
    DataCache.get(`products?category=${params.category}`, {
      revalidate: 1800, // 30 minutes
      tags: ['products', `category-${params.category}`]
    }),
    DataCache.get('filters', {
      revalidate: 86400, // 24 hours
      tags: ['filters']
    })
  ]);

  return {
    props: {
      products,
      filters
    },
    revalidate: 1800 // ISR for the whole page
  };
}

3. Server Actions

Basic Server Actions

jsx

// app/actions/user-actions.js
'use server';

import { revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';

export async function createUser(formData) {
  const user = {
    name: formData.get('name'),
    email: formData.get('email')
  };

  // Validate data
  if (!user.name || !user.email) {
    return {
      error: 'Name and email are required'
    };
  }

  try {
    // Save to database
    const response = await fetch('https://api.example.com/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(user)
    });

    if (!response.ok) {
      throw new Error('Failed to create user');
    }

    // Revalidate cache
    revalidateTag('users');
    
    // Redirect to users page
    redirect('/users');
    
  } catch (error) {
    return {
      error: error.message
    };
  }
}

export async function updateUser(userId, formData) {
  'use server';
  
  const updates = {
    name: formData.get('name'),
    email: formData.get('email')
  };

  await fetch(`https://api.example.com/users/${userId}`, {
    method: 'PATCH',
    body: JSON.stringify(updates)
  });

  revalidateTag('users');
  revalidateTag(`user-${userId}`);
}

export async function deleteUser(userId) {
  'use server';
  
  await fetch(`https://api.example.com/users/${userId}`, {
    method: 'DELETE'
  });

  revalidateTag('users');
}

Using Server Actions in Components

jsx

// app/components/user-form.js
'use client';

import { useActionState } from 'react';
import { createUser } from '../actions/user-actions';

export function UserForm() {
  const [state, formAction, isPending] = useActionState(createUser, null);

  return (
    <form action={formAction}>
      <div>
        <label htmlFor="name">Name:</label>
        <input type="text" id="name" name="name" required />
      </div>
      
      <div>
        <label htmlFor="email">Email:</label>
        <input type="email" id="email" name="email" required />
      </div>

      {state?.error && (
        <div className="error">{state.error}</div>
      )}

      <button type="submit" disabled={isPending}>
        {isPending ? 'Creating...' : 'Create User'}
      </button>
    </form>
  );
}

// app/components/user-list.js
'use client';

import { useOptimistic } from 'react';
import { deleteUser } from '../actions/user-actions';

export function UserList({ users }) {
  const [optimisticUsers, setOptimisticUsers] = useOptimistic(
    users,
    (currentUsers, userIdToDelete) => 
      currentUsers.filter(user => user.id !== userIdToDelete)
  );

  const handleDelete = async (userId) => {
    setOptimisticUsers(userId);
    await deleteUser(userId);
  };

  return (
    <div>
      {optimisticUsers.map(user => (
        <div key={user.id} className="user-item">
          <span>{user.name} - {user.email}</span>
          <button 
            onClick={() => handleDelete(user.id)}
            className="delete-btn"
          >
            Delete
          </button>
        </div>
      ))}
    </div>
  );
}

Advanced Server Actions with Validation

jsx

// lib/validation.js
import { z } from 'zod';

export const userSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Invalid email address'),
  age: z.number().min(0, 'Age must be positive').optional()
});

// app/actions/advanced-actions.js
'use server';

import { revalidatePath } from 'next/cache';
import { userSchema } from '../../lib/validation';

export async function createUserWithValidation(prevState, formData) {
  const rawData = {
    name: formData.get('name'),
    email: formData.get('email'),
    age: formData.get('age') ? parseInt(formData.get('age')) : undefined
  };

  // Validate with Zod
  const result = userSchema.safeParse(rawData);
  
  if (!result.success) {
    const errors = result.error.flatten().fieldErrors;
    return {
      error: 'Validation failed',
      fieldErrors: errors
    };
  }

  try {
    // Save validated data
    await saveUser(result.data);
    
    revalidatePath('/users');
    return { success: true, message: 'User created successfully' };
    
  } catch (error) {
    return {
      error: 'Failed to create user',
      message: error.message
    };
  }
}

// app/components/advanced-form.js
'use client';

import { useActionState } from 'react';
import { createUserWithValidation } from '../actions/advanced-actions';

const initialState = {
  error: null,
  fieldErrors: {},
  success: false,
  message: ''
};

export function AdvancedUserForm() {
  const [state, formAction, isPending] = useActionState(
    createUserWithValidation, 
    initialState
  );

  return (
    <form action={formAction}>
      <div>
        <label htmlFor="name">Name:</label>
        <input 
          type="text" 
          id="name" 
          name="name" 
          aria-invalid={state.fieldErrors.name ? 'true' : 'false'}
        />
        {state.fieldErrors.name && (
          <span className="error">{state.fieldErrors.name.join(', ')}</span>
        )}
      </div>
      
      <div>
        <label htmlFor="email">Email:</label>
        <input 
          type="email" 
          id="email" 
          name="email" 
          aria-invalid={state.fieldErrors.email ? 'true' : 'false'}
        />
        {state.fieldErrors.email && (
          <span className="error">{state.fieldErrors.email.join(', ')}</span>
        )}
      </div>

      {state.error && !state.success && (
        <div className="error">{state.error}</div>
      )}

      {state.success && (
        <div className="success">{state.message}</div>
      )}

      <button type="submit" disabled={isPending}>
        {isPending ? 'Creating...' : 'Create User'}
      </button>
    </form>
  );
}

4. Static vs Dynamic Rendering

Static Rendering (Default)

jsx

// app/products/page.js - Static by default
async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { revalidate: 3600 } // ISR: 1 hour
  });
  return res.json();
}

export default async function ProductsPage() {
  const products = await getProducts(); // This makes the page dynamic!

  return (
    <div>
      <h1>Products</h1>
      {products.map(product => (
        <div key={product.id}>{product.name}</div>
      ))}
    </div>
  );
}

// To keep it truly static, move data fetching to generateStaticParams
// app/products/[id]/page.js
export async function generateStaticParams() {
  const products = await fetch('https://api.example.com/products').then(res => res.json());
  
  return products.map(product => ({
    id: product.id.toString()
  }));
}

export default function ProductPage({ params }) {
  // params.id is available from static generation
  return <div>Product {params.id}</div>;
}

Dynamic Rendering

jsx

// app/dashboard/page.js - Dynamic rendering
import { cookies, headers } from 'next/headers';

export const dynamic = 'force-dynamic'; // Force dynamic rendering
// or
export const revalidate = 0; // No caching

async function getUserSession() {
  const cookieStore = cookies();
  const token = cookieStore.get('session-token')?.value;
  
  const headersList = headers();
  const userAgent = headersList.get('user-agent');

  const user = await fetch('https://api.example.com/user', {
    headers: { Authorization: `Bearer ${token}` },
    cache: 'no-store' // Don't cache user-specific data
  }).then(res => res.json());

  return { user, userAgent };
}

export default async function Dashboard() {
  const { user, userAgent } = await getUserSession();

  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <p>You're using: {userAgent}</p>
    </div>
  );
}

Mixed Static and Dynamic

jsx

// app/blog/[slug]/page.js
export async function generateStaticParams() {
  // Pre-render popular posts
  const popularPosts = await fetch('https://api.example.com/posts/popular', {
    next: { tags: ['posts'] }
  }).then(res => res.json());

  return popularPosts.map(post => ({
    slug: post.slug
  }));
}

export const dynamicParams = true; // Allow dynamic params not in generateStaticParams

async function getPost(slug) {
  const post = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { revalidate: 3600 } // Cache for 1 hour
  }).then(res => res.json());

  return post;
}

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug);

  if (!post) {
    notFound(); // Show 404 for non-existent posts
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

export async function generateMetadata({ params }) {
  const post = await getPost(params.slug);
  
  return {
    title: post.title,
    description: post.excerpt
  };
}

5. Incremental Static Regeneration (ISR)

Basic ISR Implementation

jsx

// pages/products/[id].js
export async function getStaticPaths() {
  // Pre-render the top 100 products at build time
  const topProducts = await fetch('https://api.example.com/products/top-100')
    .then(res => res.json());

  const paths = topProducts.map(product => ({
    params: { id: product.id.toString() }
  }));

  return {
    paths,
    fallback: 'blocking' // Generate new pages on-demand
  };
}

export async function getStaticProps({ params }) {
  try {
    const product = await fetch(`https://api.example.com/products/${params.id}`, {
      next: { revalidate: 60 } // Revalidate every minute
    }).then(res => {
      if (!res.ok) throw new Error('Product not found');
      return res.json();
    });

    return {
      props: { product },
      revalidate: 60 // ISR: 60 seconds
    };
  } catch (error) {
    return {
      notFound: true
    };
  }
}

On-Demand ISR with Revalidation

jsx

// pages/api/revalidate.js
export default async function handler(req, res) {
  // Check for secret token
  if (req.query.secret !== process.env.REVALIDATION_TOKEN) {
    return res.status(401).json({ message: 'Invalid token' });
  }

  try {
    const path = req.query.path;
    
    // Revalidate specific path
    await res.revalidate(path);
    
    return res.json({ revalidated: true });
  } catch (err) {
    return res.status(500).send('Error revalidating');
  }
}

// lib/revalidate.js
export async function revalidateProduct(productId) {
  try {
    await fetch(`/api/revalidate?secret=${process.env.REVALIDATION_TOKEN}&path=/products/${productId}`);
    console.log(`Revalidated product ${productId}`);
  } catch (error) {
    console.error('Revalidation failed:', error);
  }
}

// app/actions/product-actions.js
'use server';

import { revalidatePath, revalidateTag } from 'next/cache';

export async function updateProduct(productId, formData) {
  try {
    // Update product in database
    await fetch(`https://api.example.com/products/${productId}`, {
      method: 'PUT',
      body: JSON.stringify(Object.fromEntries(formData))
    });

    // Revalidate multiple ways
    revalidatePath('/products'); // Revalidate products list
    revalidatePath(`/products/${productId}`); // Revalidate specific product page
    revalidateTag('products'); // Revalidate all data with this tag
    
    return { success: true };
  } catch (error) {
    return { error: error.message };
  }
}

Advanced ISR Patterns

jsx

// pages/blog/[slug].js - ISR with fallback
export async function getStaticPaths() {
  // Only pre-render featured posts
  const featuredPosts = await fetch('https://api.example.com/posts/featured')
    .then(res => res.json());

  const paths = featuredPosts.map(post => ({
    params: { slug: post.slug }
  }));

  return {
    paths,
    fallback: true // Show loading state for non-pre-rendered pages
  };
}

export async function getStaticProps({ params }) {
  try {
    const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
      next: { revalidate: 300 } // 5 minutes
    }).then(res => {
      if (!res.ok) return null;
      return res.json();
    });

    if (!post) {
      return {
        notFound: true,
        revalidate: 60 // Try again in 1 minute
      };
    }

    return {
      props: { post },
      revalidate: 300 // 5 minutes
    };
  } catch (error) {
    return {
      notFound: true,
      revalidate: 60
    };
  }
}

export default function BlogPost({ post }) {
  const router = useRouter();

  // Show loading state for fallback pages
  if (router.isFallback) {
    return <div>Loading...</div>;
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

ISR with Time-Based and On-Demand Revalidation

jsx

// pages/news/[category].js
export async function getStaticPaths() {
  const categories = await fetch('https://api.example.com/news/categories')
    .then(res => res.json());

  const paths = categories.map(category => ({
    params: { category: category.slug }
  }));

  return {
    paths,
    fallback: 'blocking'
  };
}

export async function getStaticProps({ params }) {
  const news = await fetch(`https://api.example.com/news/${params.category}`, {
    next: { 
      revalidate: 60, // Time-based: 1 minute
      tags: [`news-${params.category}`] // For on-demand revalidation
    }
  }).then(res => res.json());

  return {
    props: {
      news,
      category: params.category,
      lastUpdated: new Date().toISOString()
    },
    revalidate: 60 // ISR
  };
}

// API route for on-demand revalidation
// pages/api/revalidate-news.js
export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ message: 'Method not allowed' });
  }

  const { category, secret } = req.body;

  if (secret !== process.env.REVALIDATION_SECRET) {
    return res.status(401).json({ message: 'Invalid secret' });
  }

  try {
    // Revalidate specific news category
    await res.revalidate(`/news/${category}`);
    
    // Also revalidate news homepage
    await res.revalidate('/news');
    
    return res.json({ 
      revalidated: true,
      paths: [`/news/${category}`, '/news']
    });
  } catch (err) {
    return res.status(500).json({ 
      message: 'Error revalidating',
      error: err.message 
    });
  }
}

Performance Optimization Summary

Choosing the Right Strategy

Use Case Data Fetching Caching Revalidation
Blog posts SSG/ISR revalidate: 3600 On-demand + time-based
User dashboard SSR/Client cache: 'no-store' Real-time
E-commerce products ISR revalidate: 300 On-demand
Analytics Client SWR polling Real-time
News feed ISR revalidate: 60 Frequent time-based

Best Practices

  1. Use SSG/ISR for public, cacheable content

  2. Use SSR for personalized, user-specific data

  3. Use Client-side for real-time updates and user interactions

  4. Implement proper cache headers and revalidation strategies

  5. Use Server Actions for mutations with automatic revalidation

  6. Monitor cache hit ratios and adjust revalidation times accordingly

This extensive guide covers all aspects of Next.js data fetching and caching, providing you with the tools to build highly performant applications with optimal caching strategies.

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?