React 19: Server Actions & Cập nhật Optimistic (Không độ trễ)

Cái bẫy tương tác 300ms
Khi người dùng nhấn "Thích", thêm sản phẩm vào giỏ hàng, hoặc gửi một bình luận, việc chờ đợi 200–500ms cho một phản hồi mạng hai chiều (round-trip) khiến ứng dụng của bạn có cảm giác chậm chạp và không phản hồi. Nhưng việc thay đổi trạng thái cục bộ một cách mù quáng mà không có cơ chế rollback tự động sẽ tạo ra "dữ liệu ma" (ghost data) khi request thất bại.
React 19 giải quyết vấn đề này một cách nguyên bản (natively). Bằng cách kết hợp Server Actions với các hook useOptimistic và useActionState, React mang lại độ trễ cảm nhận tức thì 0ms, đồng thời đảm bảo tính nhất quán tuyệt đối của trạng thái nếu một tác vụ thay đổi dữ liệu ở backend thất bại.
Trong bài phân tích chuyên sâu này, chúng ta sẽ khám phá cách React 19 điều phối các transition đồng thời, thực thi phía server, và rollback optimistic tức thì mà không cần đến các thư viện quản lý trạng thái của bên thứ ba.
Bộ ba (Triad) thay đổi trạng thái trong React 19
React 19 giới thiệu ba thành phần cơ bản chuyên biệt hoạt động cùng nhau để thay thế việc quản lý trạng thái form cồng kềnh:
| Hook / Primitive | Trách nhiệm chính | Nơi chạy | Tính năng chính |
|---|---|---|---|
| Server Actions | Thay đổi dữ liệu phía server & ghi vào DB | Chỉ môi trường server | Cải tiến lũy tiến (Progressive enhancement), tự động serialize FormData |
| useActionState | Trạng thái action của form, xử lý lỗi & trạng thái chờ (pending) | Client Component | Trả về [state, formAction, isPending] |
| useOptimistic | Render UI tức thì với độ trễ bằng không | Client Component (bên trong Transition) | Tự động hoàn tác về trạng thái đúng của server nếu action bị từ chối (reject) |
Vòng đời & Luồng Rollback của Optimistic Update
Điều gì xảy ra đằng sau hậu trường khi người dùng gửi một form trên kết nối 3G chậm hoặc gặp phải timeout từ máy chủ?
Kiến trúc Production hoàn chỉnh: Lượt thích & Bình luận
"use server";
import { revalidatePath } from "next/cache";
export type ActionResponse = {
success: boolean;
likesCount?: number;
error?: string;
};
export async function toggleLikeAction(
postId: string,
currentLikes: number
): Promise<ActionResponse> {
try {
// Giả lập ghi vào cơ sở dữ liệu
await new Promise((resolve) => setTimeout(resolve, 800));
// Giả lập lỗi ngẫu nhiên để minh họa khả năng rollback mạnh mẽ
if (Math.random() < 0.1) {
throw new Error("Network timeout while updating likes table");
}
const updatedLikes = currentLikes + 1;
revalidatePath(`/blog/${postId}`);
return { success: true, likesCount: updatedLikes };
} catch (err: any) {
return { success: false, error: err.message || "Failed to register like" };
}
}
"use client";
import { useOptimistic, useTransition, useState } from "react";
import { toggleLikeAction } from "@/actions/postActions";
interface LikeButtonProps {
postId: string;
initialLikes: number;
}
export function LikeButton({ postId, initialLikes }: LikeButtonProps) {
const [likes, setLikes] = useState(initialLikes);
const [isPending, startTransition] = useTransition();
// useOptimistic phản ánh ngay lập tức ý định của người dùng
const [optimisticLikes, setOptimisticLikes] = useOptimistic(
likes,
(current, increment: number) => current + increment
);
const handleLike = async () => {
startTransition(async () => {
// 1. Tăng UI ngay lập tức ở phía client
setOptimisticLikes(1);
// 2. Thực hiện Server Action
const result = await toggleLikeAction(postId, likes);
if (result.success && result.likesCount !== undefined) {
// 3. Xác nhận trạng thái từ server
setLikes(result.likesCount);
} else {
// 4. Rollback xảy ra tự động vì state không được cập nhật!
alert(`Error: ${result.error || "Could not save like"}`);
}
});
};
return (
<button
onClick={handleLike}
disabled={isPending}
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full bg-red-50 dark:bg-red-950/40 text-red-600 dark:text-red-400 font-semibold hover:scale-105 active:scale-95 transition-all shadow-sm disabled:opacity-75"
>
<span>❤️</span>
<span>{optimisticLikes} Likes</span>
{isPending && <span className="animate-pulse text-xs text-gray-400">Saving...</span>}
</button>
);
}
Checklist 4 bước cho UI Optimistic hoàn hảo
1. Luôn bọc trong một Transition
Các giá trị từ useOptimistic chỉ giữ trạng thái tạm thời của chúng trong khi một React transition đang hoạt động. Luôn gọi action của bạn bên trong startTransition hoặc truyền trực tiếp vào <form action={...}>.
2. Tạo ID tạm thời ổn định
Khi thêm các mục vào danh sách một cách optimistic, hãy tạo một UUID tạm thời (crypto.randomUUID()) để React có thể key các mục trong danh sách một cách chính xác trước khi server tạo ra ID thực sự từ cơ sở dữ liệu.
3. Xử lý cả lỗi Rejection và lỗi từ Server
Server Actions nên trả về các đối tượng có cấu trúc như { success: false, error: "..." } thay vì để promise bị reject không được xử lý, để các component phía client có thể hiển thị thông báo thân thiện (toast) trong quá trình rollback.
4. Chỉ Revalidate những gì đã thay đổi
Sử dụng revalidatePath hoặc revalidateTag một cách chi tiết trong Next.js Server Actions để tránh làm mất hiệu lực toàn bộ cache của trang và buộc phải re-render toàn trang tốn kém.
Flashcard về Mô hình tư duy
Cập nhật UI Optimistic
Cập nhật UI Optimistic
Hook useOptimistic
Hook useOptimistic
Cải tiến lũy tiến (Progressive Enhancement)
Cải tiến lũy tiến (Progressive Enhancement)
Kiểm tra kiến thức tương tác
Điều gì khiến giá trị optimistic trong `useOptimistic` của React 19 hoàn tác về trạng thái ban đầu?
Các câu hỏi thường gặp
Không. useOptimistic được thiết kế đặc biệt để xử lý các tác vụ thay đổi dữ liệu đang chờ (pending mutations) trong các transition bất đồng bộ. Mặc dù nó thay thế các thư viện fetching phía client cho việc gửi form, các store toàn cục như Zustand vẫn là lựa chọn lý tưởng cho các trạng thái chỉ tồn tại ở client và cần chia sẻ giữa nhiều trang (như chuyển đổi theme sáng/tối hoặc trình phát âm thanh).
Có! Bạn có thể gọi Server Actions bên trong các trình xử lý onClick, useEffect, hoặc các custom hook bằng cách bọc lệnh gọi trong startTransition(async () => { await myAction(); }).
React 19 cung cấp trạng thái isPending thông qua useActionState và useFormStatus, cho phép bạn vô hiệu hóa các nút gửi hoặc thực hiện debounce cho các input trong khi một server action đang được xử lý.
Bài viết liên quan
Công cụ lập trình trực tiếp trên trình duyệt
Làm sạch log AI CLI, tạo biểu thức cron, giải mã JWT và tính quyền chmod offline.
Bài viết liên quan

React 19 Actions trong thực tế: useActionState, useOptimistic & khả năng phục hồi của Server Action
Hướng dẫn toàn diện về React 19 Actions trong thực tế: useActionState, useOptimistic và khả năng phục hồi của Server Action với kiến trúc cấp độ sản phẩm và ví dụ mã.
Đọc tiếp
Next.js 15 Cache Components & Server Actions: Hướng dẫn kiến trúc sản xuất hoàn chỉnh
Hướng dẫn toàn diện về các thành phần cache và server actions của Next.js 15: hướng dẫn kiến trúc sản xuất hoàn chỉnh với kiến trúc cấp độ sản xuất và các ví dụ code.
Đọc tiếp
tRPC v11 với Next.js 15: An toàn kiểu dữ liệu từ đầu đến cuối, Server Actions & TanStack Query v5
Hướng dẫn toàn diện về tRPC v11 với Next.js 15: an toàn kiểu dữ liệu từ đầu đến cuối, Server Actions & TanStack Query v5 với kiến trúc cấp độ sản phẩm và các ví dụ code.
Đọc tiếp