Đang tải…
Đang tải…
cache() và unstable_cache() nghe như hai tên gọi của một thứ. Chúng giải hai bài toán khác nhau, và trộn lẫn thì sinh ra một lỗi TypeError rất khó chịu.

Trang bài viết chạy hoàn hảo. Mở lại lần thứ hai thì đổ, với một dòng lỗi kiểu createdAt.toISOString is not a function.
Không có gì thay đổi giữa hai lần tải. Trừ một chuyện: lần thứ hai đọc từ cache.
Next có hai cơ chế cache dễ nhầm với nhau, vì tên chúng gần giống nhau và cùng bọc quanh một hàm đọc dữ liệu.
cache() của React gộp các lời gọi trùng nhau trong cùng một request. Đây là thứ ngăn trang bài viết gọi database hai lần: generateMetadata cần tiêu đề để dựng thẻ meta, thân trang cần chính bài đó để render, và cả hai cùng gọi một hàm với cùng một slug. Không có nó là hai lượt truy vấn cho một trang.
unstable_cache() giữ kết quả xuyên qua các request, cho tới khi có ai đó chủ động xoá. Đây mới là tầng khiến bài viết không phải hỏi database lần nào nữa sau lần đầu.
Chúng lồng vào nhau, tầng ngoài là cache():
const getPostRaw = unstable_cache(
async (slug) => { /* truy vấn Supabase */ },
["published-post-by-slug"],
{ tags: ["posts"], revalidate: false }
);
export const getPost = cache(async (slug) => {
const raw = await getPostRaw(slug);
return raw;
});
revalidate: false nghĩa là giữ vô thời hạn. Dữ liệu chỉ mới lại khi một hành động ghi gọi revalidateTag("posts").
Và đây là nguồn của lỗi ở đầu bài. unstable_cache tuần tự hoá kết quả sang JSON để cất đi. JSON không có kiểu ngày tháng. Một Date đi vào sẽ đi ra dưới dạng chuỗi.
Lần chạy đầu tiên, hàm trả về đúng đối tượng bạn vừa tạo, nên createdAt là một Date thật và .toISOString() chạy ngon. Lần thứ hai, giá trị đi qua vòng JSON, và createdAt giờ là một chuỗi. TypeScript không bắt được: kiểu trả về khai báo là Date, và ở tầng kiểu thì đúng như vậy thật.
Cách sửa là làm cho tầng bị tuần tự hoá chỉ chứa giá trị an toàn với JSON, rồi dựng lại kiểu giàu hơn ở tầng ngoài:
const getPostRaw = unstable_cache(
async (slug) => ({ ...row, createdAt: row.created_at ?? null }), // chuỗi ISO
["published-post-by-slug"],
{ tags: ["posts"], revalidate: false }
);
export const getPost = cache(async (slug) => {
const raw = await getPostRaw(slug);
if (!raw) return null;
return { ...raw, createdAt: raw.createdAt ? new Date(raw.createdAt) : null };
});
Ranh giới tuần tự hoá giờ nằm ở chỗ nhìn thấy được, và mọi hàm gọi bên ngoài vẫn nhận được Date như trước.
Cái bẫy thứ hai đắt hơn, vì nó không đổ vỡ. Nó chỉ trả về dữ liệu cũ.
revalidatePath("/blog") xoá bản HTML đã render của route đó. Nó không đụng tới một entry unstable_cache gắn tag. Trang được dựng lại, gọi lại hàm đọc dữ liệu, và hàm đó vui vẻ trả về đúng bản cache cũ. Bạn thấy một trang mới tinh chứa nội dung cũ mèm.
Nên mọi hành động ghi phải gọi cả hai, và revalidateTag là dòng dễ quên:
function revalidateBlogPaths(slug) {
locales.forEach((locale) => {
revalidatePath(`/${locale}/blog`);
if (slug) revalidatePath(`/${locale}/blog/${slug}`);
});
revalidateTag("posts");
}
Phần bình luận của bài viết chỉ bọc cache(), không bọc unstable_cache(). Đó là lựa chọn có chủ ý.
Kiểm duyệt bình luận xoá cache bằng revalidatePath, mà như trên thì lệnh đó không chạm được tới entry gắn tag. Thêm tầng cache xuyên request vào đây sẽ tạo ra tình huống: duyệt một bình luận, trang dựng lại, bình luận vẫn không hiện. Giữ nguyên phần đọc ở trạng thái sống là cách rẻ nhất để nó luôn đúng.
Rút gọn lại thành một câu: dùng cache() để khỏi hỏi hai lần trong một request, dùng unstable_cache() để khỏi hỏi lại ở request sau, và nếu bạn không chắc mình sẽ nhớ xoá tag ở mọi đường ghi thì đừng thêm tầng thứ hai.
Chưa có bình luận nào — hãy là người đầu tiên!