Lesson 03-Next.js Application Routing-Data Fetching

Data Fetching, Caching, and Revalidation

Data Fetching

Next.js 14’s App Router provides multiple methods for fetching data, including static generation, server-side rendering, and client-side rendering. Each method and its use cases are detailed below.

Static Generation (getStaticProps)

Static generation fetches data at build time or during revalidation, ideal for infrequently changing data like blog posts or product lists.

// app/posts/[id]/page.js
import { getPostById } from '@/lib/posts'; // Assume this is a function to fetch post data

export async function generateStaticParams() {
  const posts = await getPosts(); // Fetch all post IDs
  return posts.map((post) => ({ id: post.id.toString() }));
}

export async function generateMetadata({ params }) {
  const post = await getPostById(params.id);
  return {
    title: post.title,
    revalidate: 60, // Revalidate every 60 seconds
  };
}

export default async function PostPage({ params }) {
  const post = await getPostById(params.id);

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

Server-Side Rendering (getServerSideProps)

Server-side rendering fetches data on each request, suitable for frequently changing data like news updates or user information.

// app/api/posts/route.js
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const posts = await fetchPosts(); // Assume this fetches a list of posts
  return NextResponse.json(posts);
}

Client-Side Rendering (useEffect)

Client-side rendering fetches data during runtime on the client, ideal for interactive data updates like real-time chats or comments.

// app/page.js
import { useEffect, useState } from 'react';

export default function HomePage() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    async function fetchData() {
      const response = await fetch('/api/posts');
      const data = await response.json();
      setPosts(data);
    }

    fetchData();
  }, []);

  return (
    <div>
      <h1>Latest Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <a href={`/posts/${post.id}`}>{post.title}</a>
          </li>
        ))}
      </ul>
    </div>
  );
}

Caching

Default Caching Behavior

  • When using fetch or other APIs to retrieve data, the default caching strategy is force-cache.
  • This means the browser attempts to retrieve data from the cache without revalidating its validity.
  • If the data is not in the cache or has expired, a network request is initiated.

Optional Caching Behaviors

  • In addition to force-cache, you can use the no-store strategy, which prevents data from being cached.
  • The stale-while-revalidate strategy allows the browser to return cached data immediately while revalidating it in the background.

Configuring Caching

Specify the caching strategy in a fetch request using the cache option:

const data = await fetch(url, { cache: 'force-cache' });

Server-Side and Client-Side Caching

  • In server-side rendering (SSR), Next.js uses caching mechanisms to optimize performance.
  • Client-side rendering (CSR) typically does not involve server-side caching, but you can control caching strategies with fetch in the App Router.

Example

Using the stale-while-revalidate strategy:

const response = await fetch(url, { cache: 'stale-while-revalidate' });

Notes

  • Different caching strategies suit different scenarios. For frequently changing data, no-store may be more appropriate.
  • For less frequently updated data, stale-while-revalidate improves initial load speed while ensuring data freshness.

Revalidation

Revalidation is a powerful feature that efficiently manages data freshness and caching between the client and server.

Revalidation Concept

  • Revalidation is a caching strategy that allows the browser to return cached data immediately while validating its validity in the background.
  • If the data is outdated, the server fetches the latest data and updates the cache.
  • This strategy enhances user experience by reducing wait times.

Types of Revalidation

  • Stale-While-Revalidate: The browser returns cached data first, then validates its validity in the background.
  • No-Store: Data is not cached, and the latest data is fetched from the server each time.

Implementation

  • In Next.js, revalidation is achieved by setting the cache option in fetch calls.
  • These features can be used in pages or components within the app directory.

Using Stale-While-Revalidate

For an API endpoint /api/data, use the stale-while-revalidate strategy on the client:

// app/page.js
export default async function Page() {
  // Use stale-while-revalidate strategy
  const res = await fetch('/api/data', { cache: 'stale-while-revalidate' });

  if (!res.ok) {
    throw new Error('Failed to fetch data');
  }

  const data = await res.json();
  return (
    <div>
      <h1>Data</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

Using No-Store

To ensure data is always fresh, use the no-store strategy:

// app/page.js
export default async function Page() {
  // Use no-store strategy
  const res = await fetch('/api/data', { cache: 'no-store' });

  if (!res.ok) {
    throw new Error('Failed to fetch data');
  }

  const data = await res.json();
  return (
    <div>
      <h1>Data</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

Server-Side Revalidation

On the server, use Next.js’s dynamic data fetching features for revalidation.

Server Components

In server components, use fetch to retrieve data with specified caching strategies:

// app/page.js
export default async function Page() {
  const res = await fetch('/api/data', { cache: 'stale-while-revalidate' });

  if (!res.ok) {
    throw new Error('Failed to fetch data');
  }

  const data = await res.json();
  return (
    <div>
      <h1>Data</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

Incremental Static Regeneration (ISR)

For statically generated content, use ISR for revalidation:

// app/page.js
export async function getStaticProps() {
  const res = await fetch('/api/data');
  const data = await res.json();

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

export default function Page({ data }) {
  return (
    <div>
      <h1>Data</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

Server Operations and Mutations

Server Operations

Server operations refer to tasks executed on the server, including but not limited to:

  • Installing and configuring operating systems
  • Configuring network settings
  • Setting up firewalls and security
  • Installing and configuring services
  • Monitoring and optimizing performance
  • Backing up and restoring data
  • Monitoring and troubleshooting
  • Updating and upgrading systems

These operations ensure the server runs smoothly and provides stable services to users.

Mutations

In software development, “mutation” typically relates to data changes, particularly in frontend development and API interactions. Key concepts include:

  • GraphQL Mutations: In GraphQL, mutations modify data, used for creating, updating, or deleting data, unlike queries which retrieve data.
  • React State Mutations: In React, state changes are referred to as state mutations, triggering component re-renders.
  • Database Mutations: In databases, mutations refer to data changes like INSERT, UPDATE, and DELETE operations.

Examples

Server Operations Example

To configure a new server, follow these basic steps:

  1. Install Operating System: Install a server OS like Ubuntu Server using a CD, USB, or other media.
  2. Configure Network: Set the server’s IP address, subnet mask, and default gateway, and configure DNS.
  3. Install Services: Use a package manager to install necessary services like web servers (Apache or Nginx) or databases (MySQL or PostgreSQL).
  4. Security Configuration: Set firewall rules and install security software like ClamAV.
  5. Performance Monitoring: Use tools like Prometheus and Grafana to monitor server performance.
  6. Backup: Set up regular backups, e.g., using rsync to sync files to another server.
  7. Troubleshooting: Diagnose issues using log files and monitoring data.

GraphQL Mutations Example

To add a new blog post using a GraphQL API:

mutation AddBlogPost($title: String!, $content: String!) {
  addBlogPost(title: $title, content: $content) {
    id
    title
    content
  }
}

Call this mutation from the client:

const mutation = `
  mutation AddBlogPost($title: String!, $content: String!) {
    addBlogPost(title: $title, content: $content) {
      id
      title
      content
    }
  }
`;

const variables = {
  title: "My First Blog Post",
  content: "This is the content of my first blog post."
};

fetch('/graphql', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: mutation,
    variables: variables,
  }),
}).then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));

Summary

  • Server Operations: Involve server management and maintenance tasks.
  • Mutations: Refer to data modification operations, critical in frontend development.

Data Fetching Methods

Data Fetching in Server Components

Server components are React components running on the server, ideal for data fetching due to their access to the server environment.

// app/page.js
import { groq } from "next-sanity";
import client from "@/lib/sanity.client";

export default async function Page() {
  const query = groq`*[_type == "post"] {
    title,
    author->name,
    _id
  }`;

  const posts = await client.fetch(query);

  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post._id}>
            <h2>{post.title}</h2>
            <p>By {post.author.name}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

Data Fetching in Dynamic Routes

Dynamic routes allow fetching data based on parameters using generateStaticParams and generateDynamicParams.

// app/posts/[id]/page.js
import { groq } from "next-sanity";
import client from "@/lib/sanity.client";

export async function generateStaticParams() {
  const query = groq`*[_type == "post"]{_id}`;
  const posts = await client.fetch(query);
  return posts.map((post) => ({ id: post._id }));
}

export default async function PostPage({ params }) {
  const query = groq`*[_type == "post" && _id == $id][0] {
    title,
    content,
    author->name,
    _id
  }`;
  const post = await client.fetch(query, { id: params.id });

  return (
    <div>
      <h1>{post.title}</h1>
      <p>By {post.author.name}</p>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </div>
  );
}

Data Fetching in Client Components

Client components run on the client, suitable for handling user interactions and state management, using useEffect and fetch for data fetching.

// app/page.js
import { useEffect, useState } from "react";

export default function Page() {
  const [data, setData] = useState(null);

  useEffect(() => {
    async function fetchData() {
      const res = await fetch("/api/data");
      if (!res.ok) {
        throw new Error("Failed to fetch data");
      }
      const data = await res.json();
      setData(data);
    }

    fetchData().catch(console.error);
  }, []);

  return (
    <div>
      <h1>Data</h1>
      {data ? (
        <pre>{JSON.stringify(data, null, 2)}</pre>
      ) : (
        <p>Loading...</p>
      )}
    </div>
  );
}

Server Actions

Server actions enable asynchronous operations on the server, such as data fetching or submission.

// app/page.js
import { groq } from "next-sanity";
import client from "@/lib/sanity.client";

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

  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post._id}>
            <h2>{post.title}</h2>
            <p>By {post.author.name}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

async function getPosts() {
  const query = groq`*[_type == "post"] {
    title,
    author->name,
    _id
  }`;
  const posts = await client.fetch(query);
  return posts;
}

Using use for Navigation

In Next.js 14, use navigation to prefetch data, improving navigation performance between pages.

// app/page.js
import Link from "next/link";

export default function Page() {
  return (
    <div>
      <h1>Posts</h1>
      <ul>
        <li>
          <Link href="/posts/1" prefetch>
            Post 1
          </Link>
        </li>
        <li>
          <Link href="/posts/2" prefetch>
            Post 2
          </Link>
        </li>
      </ul>
    </div>
  );
}

Data Fetching in Client Components

Client components use React Hooks for data fetching and state management, leveraging useEffect and fetch.

// app/page.js
import { useEffect, useState } from "react";

export default function Page() {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchData() {
      try {
        const res = await fetch("/api/data");
        if (!res.ok) {
          throw new Error("Failed to fetch data");
        }
        const data = await res.json();
        setData(data);
      } catch (error) {
        setError(error.message);
      }
    }

    fetchData();
  }, []);

  return (
    <div>
      <h1>Data</h1>
      {data ? (
        <pre>{JSON.stringify(data, null, 2)}</pre>
      ) : error ? (
        <p>Error: {error}</p>
      ) : (
        <p>Loading...</p>
      )}
    </div>
  );
}

Error Handling

Handle potential errors during data fetching using try...catch blocks to display appropriate error messages to users.

// app/page.js
import { useEffect, useState } from "react";

export default function Page() {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchData() {
      try {
        const res = await fetch("/api/data");
        if (!res.ok) {
          throw new Error("Failed to fetch data");
        }
        const data = await res.json();
        setData(data);
      } catch (error) {
        setError(error.message);
      }
    }

    fetchData();
  }, []);

  return (
    <div>
      <h1>Data</h1>
      {data ? (
        <pre>{JSON.stringify(data, null, 2)}</pre>
      ) : error ? (
        <p>Error: {error}</p>
      ) : (
        <p>Loading...</p>
      )}
    </div>
  );
}

Using SWR (Stale While Revalidate) Library

SWR is a React data fetching library that simplifies data retrieval and provides automatic revalidation.

// app/page.js
import useSWR from "swr";

function fetcher(url) {
  return fetch(url).then((res) => res.json());
}

export default function Page() {
  const { data, error } = useSWR("/api/data", fetcher);

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

  return (
    <div>
      <h1>Data</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}
Share your love