Series 5 – Rendering – Data Fetching

 

Bài 5: Server Components, Client Components và Component Boundaries trong Next.js

1. Mở đầu

Khi làm việc với React, chúng ta thường xây dựng component bằng JSX, sử dụng useState để quản lý trạng thái và useEffect để xử lý các tác vụ phụ. Phần lớn logic giao diện được thực thi trong trình duyệt của người dùng.

Tuy nhiên, khi chuyển sang Next.js App Router, bạn sẽ gặp hai khái niệm quan trọng: Server Components và Client Components.

Sự khác biệt giữa chúng không chỉ nằm ở nơi component được render, mà còn liên quan đến cách lấy dữ liệu, sử dụng JavaScript, truy cập API của trình duyệt và tối ưu hiệu suất.

Để hiểu rõ cơ chế này, chúng ta cần tìm hiểu ba khái niệm: Server Components, Client Components và Component Boundaries.

2. Server Components là gì?

Server Components là các component được thực thi ở phía server. Trong Next.js App Router, các component trong page.tsx và layout.tsx mặc định là Server Components, trừ khi được đưa vào phạm vi Client Component thông qua ranh giới module.

Server Components phù hợp với những phần giao diện cần lấy dữ liệu, hiển thị nội dung và không yêu cầu tương tác trực tiếp bằng JavaScript phía trình duyệt.

2.1. Ví dụ về Server Component

Giả sử chúng ta cần xây dựng trang danh sách bài viết. Component có thể lấy dữ liệu từ API rồi hiển thị danh sách:

type Post = {
  id: number;
  title: string;
};

async function getPosts(): Promise<Post[]> {
  const response = await fetch(
    "https://jsonplaceholder.typicode.com/posts"
  );

  if (!response.ok) {
    throw new Error("Không thể tải danh sách bài viết");
  }

  return response.json();
}

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

  return (
    <main>
      <h1>Danh sách bài viết</h1>

      {posts.slice(0, 5).map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
        </article>
      ))}
    </main>
  );
}

Trong ví dụ này, BlogPage là một Server Component vì không có chỉ thị "use client" và nằm trong App Router.

Component có thể sử dụng async/await để lấy dữ liệu ngay trong quá trình render phía server.

Đường dẫn API trên chỉ phục vụ mục đích minh họa. Trong dự án thực tế, bạn có thể lấy dữ liệu từ API của backend hoặc truy vấn cơ sở dữ liệu thông qua lớp truy cập dữ liệu phía server.

2.2. Ưu điểm của Server Components

Server Components mang lại một số lợi ích:

Lấy dữ liệu gần nguồn dữ liệu hơn

Component có thể truy vấn cơ sở dữ liệu hoặc gọi dịch vụ nội bộ mà không cần đưa toàn bộ logic truy cập dữ liệu vào trình duyệt.

Bảo vệ thông tin nhạy cảm tốt hơn

Các thông tin như API key bí mật có thể được sử dụng trong mã phía server, miễn là chúng không bị đưa vào kết quả trả về hoặc truyền sang client.

Giảm lượng JavaScript gửi xuống trình duyệt

Phần mã của Server Component không được gửi xuống trình duyệt như một Client Component. Điều này có thể giúp giảm lượng JavaScript cần tải và thực thi.

Phù hợp với nội dung cần hiển thị sớm

Server Components có thể tham gia tạo HTML trên server, giúp trình duyệt sớm nhận được nội dung hiển thị.

Tuy nhiên, Server Components không tự động khiến mọi trang nhanh hơn. Hiệu suất thực tế còn phụ thuộc vào thời gian truy vấn dữ liệu, chiến lược cache, kích thước phản hồi và cách tổ chức giao diện.

3. Client Components là gì?

Client Components là những component cần khả năng tương tác phía trình duyệt, chẳng hạn quản lý state, xử lý sự kiện hoặc sử dụng các API như window và localStorage.

Để khai báo một Client Component, chúng ta sử dụng chỉ thị "use client" ở đầu file, trước các câu lệnh import.

3.1. Ví dụ với useState

Giả sử chúng ta muốn tạo một nút tăng số lượng lượt thích:

"use client";

import { useState } from "react";

export default function LikeButton() {
  const [likes, setLikes] = useState(0);

  return (
    <button
      type="button"
      onClick={() => setLikes((current) => current + 1)}
    >
      Lượt thích: {likes}
    </button>
  );
}

Component này cần chạy logic tương tác trong trình duyệt vì nó sử dụng useState và sự kiện onClick.

Nếu bỏ "use client" trong trường hợp này, bạn sẽ gặp lỗi do đang sử dụng tính năng chỉ dành cho Client Component trong một Server Component.

3.2. Khi nào cần Client Components?

Bạn thường cần Client Components khi sử dụng:

Tính năng Ví dụ
State useState, useReducer
Effect useEffect
Event handlers onClick, onChange
Browser APIs window, localStorage
Custom hooks phía client Hook theo dõi kích thước màn hình
Tương tác giao diện Modal, dropdown, tab, bộ lọc sản phẩm

Một điểm cần nhớ là Client Component vẫn có thể được pre-render thành HTML trên server trong lần tải trang đầu tiên. "use client" không đồng nghĩa với việc component chỉ được render trên trình duyệt.

Trong các lần điều hướng tiếp theo, Client Components có thể được render phía client theo cơ chế của Next.js.

4. Component Boundaries là gì?

Component Boundary có thể hiểu là ranh giới giữa phần mã thuộc môi trường server và phần mã thuộc môi trường client.

Trong Next.js, chỉ thị "use client" đánh dấu một điểm bắt đầu của Client Component trong đồ thị module. Các module được import từ điểm bắt đầu này cũng thuộc phạm vi client bundle, trừ những cơ chế đặc biệt do framework xử lý.

Điều này ảnh hưởng trực tiếp đến cách chúng ta tổ chức component.

4.1. Ví dụ về Component Boundary

Giả sử trang blog có header, danh sách bài viết và nút thích:

BlogPage (Server Component)
├── Header (Server Component)
├── PostList (Server Component)
│   ├── PostItem
│   └── PostItem
└── LikeButton (Client Component)

Trong cấu trúc này:

  • BlogPage lấy dữ liệu và xây dựng nội dung trang.
  • Header hiển thị phần giao diện tĩnh.
  • PostList hiển thị danh sách bài viết.
  • LikeButton xử lý tương tác của người dùng.

Cách tổ chức này giúp phần giao diện không cần tương tác không phải chuyển sang Client Component chỉ vì một nút nhỏ cần state.

4.2. Tổ chức mã nguồn thực tế

Ví dụ cấu trúc thư mục:

src/
└── app/
    ├── blog/
    │   └── page.tsx
    └── components/
        ├── Header.tsx
        ├── PostList.tsx
        └── LikeButton.tsx

File LikeButton.tsx:

"use client";

import { useState } from "react";

export default function LikeButton() {
  const [likes, setLikes] = useState(0);

  return (
    <button
      onClick={() => setLikes((value) => value + 1)}
    >
      Thích ({likes})
    </button>
  );
}

Trong page.tsx, chúng ta có thể sử dụng component này cùng các Server Components khác:

import Header from "../components/Header";
import PostList from "../components/PostList";
import LikeButton from "../components/LikeButton";

export default function BlogPage() {
  return (
    <main>
      <Header />
      <PostList />
      <LikeButton />
    </main>
  );
}

BlogPage vẫn là Server Component. Việc import và render LikeButton không làm toàn bộ BlogPage trở thành Client Component.

Đây là điểm quan trọng: Ranh giới client được xác định bởi module graph, không đơn giản là mọi component nằm bên trong một component client đều tự động trở thành cùng một module.

5. Truyền dữ liệu giữa Server Components và Client Components

Một tình huống thường gặp là dữ liệu được lấy từ server nhưng cần hiển thị trong một component có tương tác.

Ví dụ, trang blog lấy số lượt thích hiện tại từ nguồn dữ liệu rồi truyền vào nút tương tác.

Server Component:

import LikeButton from "./LikeButton";

export default async function PostPage() {
  const initialLikes = 12;

  return (
    <main>
      <h1>Bài viết về Next.js</h1>
      <LikeButton initialLikes={initialLikes} />
    </main>
  );
}

Client Component:

"use client";

import { useState } from "react";

export default function LikeButton({
  initialLikes,
}: {
  initialLikes: number;
}) {
  const [likes, setLikes] = useState(initialLikes);

  return (
    <button
      onClick={() => setLikes((value) => value + 1)}
    >
      Thích ({likes})
    </button>
  );
}

Trong ví dụ này, Server Component truyền initialLikes sang Client Component thông qua props.

Các props truyền qua ranh giới server/client phải đáp ứng yêu cầu tuần tự hóa của React. Những giá trị đơn giản như chuỗi, số, boolean và các object dữ liệu phù hợp thường là lựa chọn dễ dùng nhất.

Ví dụ trên chỉ cập nhật state trên giao diện. Nếu muốn lưu lượt thích thực tế, ứng dụng cần gửi yêu cầu đến backend hoặc sử dụng cơ chế cập nhật dữ liệu phía server.

6. Có thể đưa Server Component vào bên trong Client Component không?

Đây là một điểm dễ gây nhầm lẫn.

Bạn không thể biến một module Server Component thành Server Component thực sự chỉ bằng cách import trực tiếp module đó từ một Client Component. Khi module được import từ client entry point, nó thuộc đồ thị module phía client và có thể gây lỗi nếu sử dụng các API chỉ dành cho server.

Tuy nhiên, bạn có thể tạo một Client Component nhận nội dung được tạo ở server thông qua props như children.

Ví dụ, tạo một component modal:

"use client";

import { useState, type ReactNode } from "react";

export default function Modal({
  children,
}: {
  children: ReactNode;
}) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>
        Mở nội dung
      </button>

      {isOpen && (
        <div>
          <button onClick={() => setIsOpen(false)}>
            Đóng
          </button>

          {children}
        </div>
      )}
    </div>
  );
}

Sau đó, Server Component có thể truyền nội dung vào modal:

import Modal from "./Modal";

async function ServerContent() {
  return <p>Nội dung được tạo từ phía server.</p>;
}

export default async function Page() {
  return (
    <Modal>
      <ServerContent />
    </Modal>
  );
}

Trong ví dụ này, ServerContent được tạo ở phía server trước khi nội dung được truyền vào Client Component thông qua children.

Cách kết hợp này giúp giao diện có tương tác mà vẫn giữ được những phần nội dung phù hợp ở phía server.

7. Những sai lầm thường gặp

Thêm use client vào mọi component

Việc đánh dấu quá nhiều component là client có thể làm tăng lượng JavaScript cần gửi xuống trình duyệt và khiến ranh giới server/client khó quản lý.

Hãy bắt đầu với Server Components và chỉ chuyển những phần thực sự cần tương tác sang client.

Nghĩ rằng Client Component không thể được render trên server

Client Component vẫn có thể được pre-render thành HTML trong lần tải trang đầu tiên. Nó cần JavaScript phía client để hydration và thực hiện tương tác.

Đưa thông tin bí mật vào Client Component

Không đưa API key bí mật, thông tin kết nối cơ sở dữ liệu hoặc logic nhạy cảm vào mã phía client.

Bạn có thể sử dụng server-only để giúp ngăn module chứa logic chỉ dành cho server bị import nhầm vào Client Component.

Nghĩ rằng dữ liệu trong Server Component luôn được cache

Server Components có thể lấy dữ liệu từ server, nhưng việc dữ liệu có được cache hay không phụ thuộc vào API và cấu hình caching đang sử dụng. Không nên giả định mọi lời gọi fetch đều được cache giống nhau.

8. Bài tập thực hành

Hãy xây dựng một trang danh sách bài viết với các yêu cầu:

  1. Tạo page.tsx làm Server Component.
  2. Lấy danh sách bài viết từ một API.
  3. Tạo PostList.tsx để hiển thị dữ liệu.
  4. Tạo LikeButton.tsx làm Client Component.
  5. Truyền dữ liệu cần thiết từ Server Component sang Client Component qua props.
  6. Kiểm tra rằng nút thích có thể tương tác trong trình duyệt.
  7. Chỉ thêm "use client" vào những file cần thiết.

Sau khi hoàn thành, hãy thử đặt "use client" ở một component cấp cao hơn và quan sát cách phạm vi client bundle thay đổi.

9. Kết luận

Server Components phù hợp với việc lấy dữ liệu, xử lý logic phía server và hiển thị nội dung không cần tương tác trực tiếp. Client Components phù hợp với state, event handlers và các API của trình duyệt.

Component Boundaries giúp xác định nơi mã chuyển từ môi trường server sang client. Việc đặt ranh giới hợp lý giúp ứng dụng dễ tổ chức, tránh đưa quá nhiều JavaScript xuống trình duyệt và giữ logic nhạy cảm ở đúng môi trường.

Trong bài tiếp theo, chúng ta sẽ tìm hiểu Data Fetching, Caching và Revalidation để hiểu cách lấy dữ liệu, tái sử dụng kết quả và cập nhật nội dung trong Next.js App Router.

Tài liệu tham khảo

 

Bài 6: Data Fetching, Caching và Revalidation trong Next.js

Data Fetching, Caching và Revalidation trong Next.js

1. Mở đầu

Khi xây dựng một ứng dụng web, giao diện không chỉ bao gồm những nội dung được viết cố định trong mã nguồn. Phần lớn ứng dụng thực tế cần lấy dữ liệu từ API, cơ sở dữ liệu hoặc các dịch vụ bên ngoài.

Ví dụ, một website bán hàng cần lấy danh sách sản phẩm, giá bán và thông tin tồn kho. Một trang blog cần lấy danh sách bài viết mới nhất. Một ứng dụng quản lý công việc cần lấy dữ liệu của người dùng và cập nhật khi có thay đổi.

Trong Next.js App Router, ba khái niệm quan trọng giúp xử lý những công việc này là Data Fetching, Caching và Revalidation.

Data Fetching giúp lấy dữ liệu từ nguồn cần thiết. Caching giúp tái sử dụng dữ liệu hoặc kết quả đã được lưu. Revalidation giúp xác định khi nào dữ liệu được kiểm tra hoặc cập nhật lại để tránh hiển thị nội dung lỗi thời.

2. Data Fetching là gì?

Data Fetching là quá trình lấy dữ liệu từ một nguồn để sử dụng trong ứng dụng.

Trong Next.js, dữ liệu có thể được lấy từ nhiều nguồn khác nhau:

  • REST API hoặc các dịch vụ web.
  • Cơ sở dữ liệu thông qua thư viện phía server.
  • Hệ thống CMS quản lý nội dung.
  • Các dịch vụ nội bộ của ứng dụng.

Trong App Router, một cách phổ biến để lấy dữ liệu là sử dụng fetch bên trong Server Component.

2.1. Lấy dữ liệu trong Server Component

Giả sử chúng ta cần hiển thị danh sách bài viết từ API.

type Post = {
  id: number;
  title: string;
  body: string;
};

async function getPosts(): Promise<Post[]> {
  const response = await fetch(
    "https://jsonplaceholder.typicode.com/posts"
  );

  if (!response.ok) {
    throw new Error("Không thể tải bài viết");
  }

  return response.json();
}

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

  return (
    <main>
      <h1>Danh sách bài viết</h1>

      {posts.slice(0, 5).map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </article>
      ))}
    </main>
  );
}

Trong ví dụ này:

  1. getPosts() gửi request đến API.
  2. await response.json() chuyển dữ liệu JSON thành giá trị JavaScript.
  3. Component sử dụng dữ liệu nhận được để render giao diện.
  4. Nếu request thất bại, lỗi được ném ra để cơ chế xử lý lỗi phù hợp của ứng dụng tiếp nhận.

Vì BlogPage là Server Component nên việc lấy dữ liệu được thực hiện ở phía server, thay vì bắt buộc phải gọi API từ trình duyệt bằng useEffect.

2.2. Data Fetching trong Client Component

Không phải mọi yêu cầu lấy dữ liệu đều phải thực hiện ở server.

Client Component phù hợp khi dữ liệu cần thay đổi theo tương tác trong trình duyệt, chẳng hạn người dùng nhập từ khóa tìm kiếm hoặc nhấn nút tải lại.

Ví dụ:

"use client";

import { useState } from "react";

type User = {
  id: number;
  name: string;
};

export default function UserSearch() {
  const [userId, setUserId] = useState("1");
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");

  async function handleSearch() {
    setLoading(true);
    setError("");
    setUser(null);

    try {
      const response = await fetch(
        `https://jsonplaceholder.typicode.com/users/${encodeURIComponent(userId)}`
      );

      if (!response.ok) {
        throw new Error("Không tìm thấy người dùng");
      }

      const data: User = await response.json();
      setUser(data);
    } catch {
      setError("Không thể tải thông tin người dùng.");
    } finally {
      setLoading(false);
    }
  }

  return (
    <section>
      <input
        value={userId}
        onChange={(event) => setUserId(event.target.value)}
        placeholder="Nhập ID người dùng"
      />

      <button
        type="button"
        onClick={handleSearch}
        disabled={loading}
      >
        Tìm kiếm
      </button>

      {loading && <p>Đang tải dữ liệu...</p>}
      {error && <p>{error}</p>}
      {user && <p>{user.name}</p>}
    </section>
  );
}

Ví dụ này minh họa việc lấy dữ liệu theo thao tác của người dùng. Trong ứng dụng thực tế, cần bổ sung kiểm tra đầu vào phù hợp và xử lý các trường hợp API trả về dữ liệu không đúng định dạng.

2.3. Nên lấy dữ liệu ở server hay client?

Tiêu chí Server Component Client Component
Lấy dữ liệu ban đầu của trang Phù hợp Có thể sử dụng
Truy cập cơ sở dữ liệu trực tiếp từ server Có thể Không nên truy cập trực tiếp
Tương tác theo thao tác người dùng Không phải thế mạnh chính Phù hợp
Sử dụng useState, useEffect Không Có
Giữ bí mật thông tin truy cập server Phù hợp nếu xử lý đúng Không được đưa bí mật vào mã client

Một cách tổ chức thường gặp là lấy dữ liệu ban đầu trong Server Component, sau đó truyền dữ liệu qua props cho Client Component nếu cần tương tác.

3. Caching là gì?

Caching là kỹ thuật lưu lại dữ liệu hoặc kết quả đã xử lý để có thể tái sử dụng, thay vì thực hiện lại toàn bộ công việc mỗi lần.

Ví dụ, một website có danh sách bài viết thay đổi vài lần mỗi ngày. Nếu mọi request đều phải gọi lại dịch vụ dữ liệu và xử lý từ đầu, hệ thống có thể tốn thêm thời gian và tài nguyên.

Caching có thể giúp giảm số lần truy cập nguồn dữ liệu, giảm độ trễ và cải thiện hiệu suất. Tuy nhiên, cache cũng có thể khiến ứng dụng hiển thị dữ liệu cũ nếu không được thiết kế phù hợp.

3.1. Caching với fetch trong Next.js

Trong các phiên bản Next.js hiện hành, không nên mặc định rằng mọi lời gọi fetch trong Server Component đều được cache dữ liệu lâu dài.

Nếu muốn cache kết quả của một request theo cơ chế Data Cache, bạn có thể cấu hình rõ ràng:

const response = await fetch(
  "https://example.com/api/posts",
  {
    cache: "force-cache",
  }
);

cache: "force-cache" yêu cầu Next.js sử dụng cơ chế cache phù hợp cho request này.

Ngược lại, nếu dữ liệu cần được lấy mới theo mỗi request:

const response = await fetch(
  "https://example.com/api/account",
  {
    cache: "no-store",
  }
);

cache: "no-store" yêu cầu không sử dụng Data Cache cho request đó.

Lưu ý rằng những ví dụ trên giả định đây là các request được thực hiện trong môi trường server của Next.js. fetch chạy ở trình duyệt có cơ chế cache HTTP của trình duyệt và không sử dụng Next.js Data Cache theo cách tương tự.

3.2. Cache dữ liệu khác với cache giao diện

Caching không chỉ có một loại. Trong Next.js, cần phân biệt các cơ chế khác nhau:

Data Cache

Lưu kết quả dữ liệu được lấy từ những request phù hợp để có thể tái sử dụng.

Request Memoization

Trong quá trình render phía server, các lời gọi fetch giống nhau có thể được hợp nhất để tránh thực hiện trùng lặp trong cùng một quá trình render. Đây không phải là cache lâu dài giữa mọi request.

Full Route Cache

Có thể lưu kết quả render của những route đủ điều kiện để tái sử dụng. Việc route có được cache hay không phụ thuộc vào cấu hình và đặc điểm sử dụng dữ liệu.

Router Cache

Phía client có thể lưu các React Server Component payload đã nhận được nhằm hỗ trợ điều hướng. Cơ chế này khác với Data Cache phía server.

Các cơ chế trên giải quyết những vấn đề khác nhau. Khi debug dữ liệu chưa cập nhật, bạn cần xác định dữ liệu đang được lưu ở lớp nào thay vì chỉ tìm một tùy chọn cache duy nhất.

4. Revalidation là gì?

Revalidation là quá trình làm mới hoặc kiểm tra lại dữ liệu đã được cache để ứng dụng có thể hiển thị nội dung mới hơn.

Nếu cache giúp tái sử dụng dữ liệu, thì revalidation giúp kiểm soát thời điểm dữ liệu cần được cập nhật.

Ví dụ, một website tin tức có thể cache danh sách bài viết trong một khoảng thời gian để giảm tải cho nguồn dữ liệu, sau đó làm mới cache theo lịch hoặc khi có bài viết mới.

Next.js hỗ trợ nhiều cách revalidation, trong đó có thời gian hết hạn và cơ chế làm mới theo tag hoặc path.

4.1. Revalidation theo thời gian

Bạn có thể yêu cầu Next.js revalidate dữ liệu sau một khoảng thời gian nhất định:

const response = await fetch(
  "https://example.com/api/posts",
  {
    next: {
      revalidate: 60,
    },
  }
);

Giá trị 60 là khoảng thời gian tính bằng giây.

Cấu hình này cho phép Next.js sử dụng cache và revalidate dữ liệu theo khoảng thời gian đã đặt. Đây không phải cam kết rằng dữ liệu chắc chắn được cập nhật đúng tại giây thứ 60. Việc làm mới có thể diễn ra khi có request tiếp theo hoặc theo cơ chế cache của Next.js.

Cách này phù hợp với dữ liệu không cần cập nhật tức thời, chẳng hạn danh sách bài viết hoặc nội dung giới thiệu.

4.2. Revalidation theo tag

Khi nhiều trang cùng sử dụng một nhóm dữ liệu, cache tag giúp đánh dấu dữ liệu để có thể làm mới theo nhóm.

Ví dụ:

const response = await fetch(
  "https://example.com/api/posts",
  {
    next: {
      tags: ["posts"],
    },
  }
);

Khi dữ liệu bài viết thay đổi, ứng dụng có thể yêu cầu revalidate tag tương ứng từ phía server.

Với Next.js App Router, revalidateTag được sử dụng trong những ngữ cảnh server phù hợp, chẳng hạn Server Action hoặc Route Handler:

"use server";

import { revalidateTag } from "next/cache";

export async function refreshPosts() {
  revalidateTag("posts", "max");
}

Trong ví dụ này, "max" sử dụng chiến lược stale-while-revalidate: dữ liệu cũ có thể được trả về trong lúc cache được làm mới khi có request tiếp theo.

Đoạn code chỉ minh họa thao tác revalidation. Trong ứng dụng thực tế, hàm này cần được gọi khi có sự kiện phù hợp, chẳng hạn sau khi nội dung được cập nhật thành công.

Không nên gọi revalidation một cách tùy tiện trong mọi lần render vì điều đó có thể làm giảm lợi ích của caching.

4.3. Revalidation theo path

Nếu muốn làm mới dữ liệu cache liên quan đến một route cụ thể, Next.js cung cấp revalidatePath.

Ví dụ:

"use server";

import { revalidatePath } from "next/cache";

export async function refreshBlogPage() {
  revalidatePath("/blog");
}

Thao tác này yêu cầu Next.js revalidate dữ liệu cache liên quan đến đường dẫn /blog theo cơ chế tương ứng.

Điểm khác biệt dễ nhớ:

  • revalidateTag nhắm đến dữ liệu được đánh dấu bằng tag.
  • revalidatePath nhắm đến một đường dẫn hoặc phạm vi route cụ thể.

Lựa chọn nào phù hợp phụ thuộc vào cách dữ liệu được chia sẻ và cách tổ chức ứng dụng.

5. Kết hợp Data Fetching, Caching và Revalidation

Hãy xem xét một trang blog cần hiển thị danh sách bài viết. Nội dung không thay đổi từng giây, nhưng khi quản trị viên đăng bài mới, danh sách cần được cập nhật.

Chúng ta có thể xây dựng hàm lấy dữ liệu như sau:

type Post = {
  id: number;
  title: string;
};

async function getPosts(): Promise<Post[]> {
  const response = await fetch(
    "https://example.com/api/posts",
    {
      next: {
        revalidate: 300,
        tags: ["posts"],
      },
    }
  );

  if (!response.ok) {
    throw new Error("Không thể tải bài viết");
  }

  return response.json();
}

Trong ví dụ này:

  1. Data Fetching: fetch lấy danh sách bài viết.
  2. Caching: kết quả có thể được lưu trong Data Cache của Next.js.
  3. Revalidation: dữ liệu được thiết lập để revalidate theo khoảng thời gian 300 giây.
  4. Tag: dữ liệu được gắn nhãn posts để có thể chủ động revalidate theo tag khi cần.

Khi quản trị viên tạo hoặc cập nhật bài viết, backend hoặc một Server Action được bảo vệ phù hợp có thể yêu cầu revalidate tag posts.

Lưu ý rằng example.com là địa chỉ minh họa. API thực tế cần hỗ trợ dữ liệu phù hợp, và việc cập nhật dữ liệu phải hoàn thành thành công trước khi thực hiện revalidation.

6. Chọn chiến lược nào cho từng loại dữ liệu?

Không phải dữ liệu nào cũng nên được cache theo cùng một cách.

Loại dữ liệu Hướng tiếp cận gợi ý
Bài viết blog ít thay đổi Cache và revalidate theo thời gian hoặc tag
Nội dung giới thiệu công ty Có thể cache trong thời gian dài
Danh sách sản phẩm Cache theo nhu cầu cập nhật và yêu cầu kinh doanh
Giá và tồn kho cần độ mới cao Xác định rõ chính sách cache; có thể cần lấy dữ liệu mới theo request
Thông tin tài khoản riêng tư Tránh cache dùng chung giữa người dùng; kiểm soát quyền truy cập
Kết quả tìm kiếm thay đổi theo thao tác Lấy dữ liệu theo yêu cầu và quản lý trạng thái phía client khi phù hợp

Đây chỉ là những gợi ý ban đầu. Chiến lược cuối cùng cần dựa vào yêu cầu về độ mới của dữ liệu, tải hệ thống, bảo mật và khả năng chấp nhận dữ liệu cũ.

Đặc biệt, dữ liệu cá nhân hoặc dữ liệu nhạy cảm không nên bị đưa vào cache dùng chung một cách vô tình.

7. Những sai lầm thường gặp

Nghĩ rằng mọi fetch đều được cache

Không nên mặc định điều đó. Hãy kiểm tra cấu hình cụ thể của request và phiên bản Next.js đang sử dụng.

Nhầm lẫn giữa cache và revalidation

Cache là cơ chế lưu trữ để tái sử dụng. Revalidation là cách làm mới hoặc kiểm tra lại dữ liệu được cache.

Dùng cache quá lâu cho dữ liệu thay đổi thường xuyên

Dữ liệu như tồn kho hoặc trạng thái giao dịch có thể cần chính sách khác với nội dung blog. Cần xác định rõ mức độ chấp nhận dữ liệu cũ.

Chỉ cập nhật cơ sở dữ liệu mà quên cache

Nếu dữ liệu đã được cache, việc cập nhật nguồn dữ liệu không nhất thiết khiến mọi kết quả cache liên quan lập tức thay đổi. Bạn cần thiết kế cơ chế invalidation hoặc revalidation phù hợp.

Nghĩ rằng revalidation là xác thực dữ liệu

Revalidation không thay thế xác thực người dùng hay phân quyền. Nó chỉ liên quan đến việc làm mới dữ liệu cache, không quyết định người dùng có được phép truy cập dữ liệu hay không.

8. Bài tập thực hành

Hãy xây dựng một trang blog bằng Next.js App Router với các yêu cầu:

  1. Tạo Server Component hiển thị danh sách bài viết lấy từ API.
  2. Tạo hàm getPosts() riêng để tái sử dụng logic lấy dữ liệu.
  3. Cấu hình cache và revalidation theo thời gian cho danh sách bài viết.
  4. Gắn tag posts cho dữ liệu.
  5. Tạo một Server Action hoặc Route Handler phù hợp để revalidate tag sau khi nội dung được cập nhật thành công.
  6. Kiểm tra kết quả khi tải lại trang và sau khi cập nhật bài viết.
  7. Thử chuyển request sang cache: "no-store" để so sánh hành vi lấy dữ liệu mới.

Khi thực hành, hãy quan sát riêng dữ liệu từ API, Data Cache và giao diện được hiển thị. Điều này giúp bạn tránh nhầm lẫn giữa việc nguồn dữ liệu đã thay đổi và việc nội dung cache đã được làm mới.

9. Kết luận

Data Fetching là bước lấy dữ liệu từ nguồn cần thiết. Caching giúp tái sử dụng dữ liệu hoặc kết quả render, còn Revalidation cung cấp cơ chế làm mới cache khi dữ liệu thay đổi hoặc khi đến thời điểm cần kiểm tra lại.

Khi kết hợp đúng ba cơ chế này, ứng dụng Next.js có thể giảm các request không cần thiết, cải thiện hiệu suất và vẫn giữ dữ liệu đủ mới theo yêu cầu của sản phẩm.

Điều quan trọng không phải là cache mọi thứ, mà là lựa chọn chiến lược phù hợp cho từng loại dữ liệu.

Trong bài tiếp theo, chúng ta sẽ tìm hiểu CSR, SSR, SSG và ISR để hiểu các chiến lược render phổ biến, cách chúng hoạt động và khi nào nên sử dụng từng chiến lược.

Tài liệu tham khảo

Bài viết khác

Series 7 — Web Quality – Engineering :Testing – Unit Testing, Integration Testing, Web Performance – Core Web Vitals, Code Splitting, Lazy Loading

Series 7 — Web Quality – Engineering Bài 3: Testing – Unit Testing, Integration Testing và End-to-End Testing trong ứng dụng web 1. Vì sao ứng dụng web cần Testing? Trong quá trình phát triển Frontend, chúng ta thường kiểm tra ứng dụng bằng cách chạy website trên trình duyệt, nhấn vào các nút, nhập […]

Series 7 — Web Quality – Engineering : Authentication & Authorization

Series 7 — Web Quality – Engineering Bài 1: Authentication & Authorization – Xác thực và phân quyền trong ứng dụng web 1. Mở đầu Khi xây dựng một ứng dụng web có chức năng đăng nhập, chúng ta thường nghĩ rằng chỉ cần tạo biểu mẫu nhập email, mật khẩu và kiểm tra thông […]

Series 6 – Next.js Advanced – Production: Next.js Build & Deployment

Series 6 – Next.js Advanced & Production Bài 5: Next.js Build & Deployment – Từ mã nguồn đến môi trường Production 1. Mở đầu Trong quá trình phát triển một ứng dụng web, việc chạy thành công trên máy tính cá nhân chưa có nghĩa là ứng dụng đã sẵn sàng để đưa đến người […]

Series 6 – Next.js Advanced – Production: Next.js Internationalized Routing Next.js Static Export

Series 6 – Next.js Advanced & Production   Bài 3: Next.js Internationalized Routing – Tổ chức URL cho website đa ngôn ngữ 1. Mở đầu Sau khi xây dựng cơ chế dịch nội dung và định dạng theo locale, ứng dụng cần một cách để người dùng truy cập đúng phiên bản ngôn ngữ. Ví […]

Series 6 – Next.js Advanced & Production: Metadata & SEO, Internationalization (i18n) & Localization (l10n)

Series 6 – Next.js Advanced & Production Bài 1: Metadata & SEO trong Next.js – Tối ưu thông tin và khả năng hiển thị trên công cụ tìm kiếm 1. Mở đầu Khi xây dựng một website, việc hoàn thiện giao diện và chức năng mới chỉ là một phần của quá trình phát triển. […]

Series 5 – Next.js Fundamentals: CSR, SSR, SSG, ISR và cách lựa chọn

Series 5 – Next.js Fundamentals   Bài 7 : CSR, SSR, SSG và ISR – Các chiến lược Rendering trong Next.js 1. Mở đầu Khi xây dựng một website bằng React, chúng ta thường tập trung vào việc tạo component, quản lý state và hiển thị giao diện trên trình duyệt. Tuy nhiên, khi ứng dụng […]

Leave a Reply

Your email address will not be published. Required fields are marked *