Bạn đang dùng Facebook, thấy một bài viết hay, bấm Like. Nút tim đổi màu ngay lập tức, không cần chờ, không có loading spinner. Nhưng thực tế phía sau đó, request vẫn đang trên đường bay tới server.
Đó chính là Optimistic Update, kỹ thuật cập nhật UI trước khi server xác nhận, dựa trên “sự lạc quan” rằng thao tác sẽ thành công. Nếu server báo lỗi, UI sẽ tự động quay về trạng thái cũ. Nếu thành công, không có gì thay đổi thêm vì UI đã đúng rồi.
Vấn đề: Server Action làm nút Like bị trễ
Trong Next.js, một cách phổ biến để xử lý Like button là dùng Server Action kết hợp với formAction trên <button>. Không cần <form> bọc ngoài, button vẫn có thể gắn action trực tiếp:
formAction là thuộc tính của HTML form — nó được thiết kế để nhận vào một URL hoặc một function đã được chuẩn bị sẵn, không nhận arrow function inline. Nếu bạn viết:
Next.js sẽ không nhận ra đây là Server Action và sẽ không xử lý đúng cách — đặc biệt là mất đi khả năng hoạt động khi JavaScript bị tắt (progressive enhancement).
bind là method của function trong JavaScript, dùng để tạo ra một function mới với.
bind trả ra một function mới cùng loại với function gốc, nên Next.js vẫn nhận ra đó là Server Action. bind(null, post.id) làm hai việc:
Argument 1 — null: set this bên trong function đó là gì. Server Action không dùng this, nên truyền null cho qua. Argument 2 trở đi — post.id: "nhét sẵn" argument vào đầu function. Kết quả: postLikeAction.bind(null, post.id) trả ra một function mới, mà khi được gọi sẽ tự động chạy postLikeAction(post.id).
Tại sao cần bind ở đây?
formAction chỉ chấp nhận một function không có argument — nó sẽ tự gọi function đó khi form submit, không truyền gì thêm. Nhưng postLikeAction cần biết post.id là bao nhiêu. bind giúp "nhúng sẵn" post.id vào function trước, để khi formAction gọi nó thì argument đã có sẵn rồi.
Luồng hoạt động như thế nào?
User bấm Like → gọi Server Action → toggleLike ghi vào DB → revalidatePath làm Next.js fetch lại dữ liệu → component re-render với post.isLiked mới.
Toàn bộ quá trình đó mất 1–2 giây.
Sau 1–2 giây thì UI vẫn cập nhật — revalidatePath làm đúng việc của nó, nút Like sẽ đổi màu và số like tăng lên.
Vấn đề là gì? Trong 1–2 giây chờ server phản hồi, nút Like không có bất kỳ phản ứng nào. Người dùng không biết click có được ghi nhận không, dễ bấm lại nhiều lần. Kết quả cuối cùng vẫn có thể đúng, nhưng trải nghiệm trong lúc chờ thì rất tệ.
Giải pháp: useOptimistic
React cung cấp hook useOptimistic để xử lý chính xác vấn đề này. Ý tưởng đơn giản:
Khi người dùng bấm Like, cập nhật UI ngay lập tức (optimistic) Song song gọi Server Action để lưu xuống DB Nếu Server Action thành công → revalidatePath trả dữ liệu thật về, thay thế trạng thái optimistic Nếu Server Action thất bại → React tự động hoàn tác UI về trạng thái ban đầu [Bấm Like]
├── UI cập nhật NGAY (optimistic)
└── Server Action chạy ngầm
├── Thành công → dữ liệu thật thay thế, không thấy sự khác biệt
└── Thất bại → UI tự rollback về trạng thái cũ
Implement từng bước
Bước 1: Tách component Posts thành Client Component
useOptimistic là một React hook, nên component sử dụng nó phải là Client Component. Thêm 'use client' vào đầu file.
Bước 2: Khởi tạo useOptimistic
useOptimistic nhận vào hai thứ:
State thật (posts): dữ liệu từ server, nguồn sự thật duy nhất Reducer function: mô tả cách tạo ra trạng thái optimistic khi có một action xảy ra optiPosts là những gì bạn render lên UI — bình thường nó bằng posts, nhưng trong lúc đợi server phản hồi, nó sẽ là phiên bản “lạc quan” đã được cập nhật sẵn.
Bước 3: Tạo hàm xử lý tích hợp
Hai dòng này chạy theo thứ tự, nhưng UI người dùng thấy là cập nhật tức thì vì updateOptiPosts là synchronous. postLikeAction chạy ngầm phía sau.
Bước 4: Truyền action xuống component Post
Lưu ý: render optiPosts thay vì posts — đây là điểm mấu chốt.
Bước 5: Cập nhật component Post
Ở đây dùng onClick thay vì formAction vì action giờ là một client function bọc ngoài, không còn là Server Action trực tiếp nữa.
Những tình huống phổ biến trong production
useOptimistic không chỉ dùng cho Like button. Đây là những nơi bạn sẽ thấy pattern này xuất hiện thường xuyên:
Todo list — Thêm task mới xuất hiện ngay trong danh sách trước khi được lưu vào DB. Nếu lỗi, task tự biến mất.
Follow/Unfollow — Bấm Follow trên Twitter/LinkedIn, số lượng follower tăng lên 1 ngay lập tức.
Thêm vào giỏ hàng — Icon giỏ hàng cập nhật số lượng ngay khi bấm, không cần chờ API xác nhận.
Đánh dấu đã đọc thông báo — Notification badge giảm xuống ngay khi click vào.
Reorder danh sách (drag & drop) — Thứ tự thay đổi ngay khi thả, lưu ngầm sau.
Điểm chung của tất cả: đây đều là những thao tác đơn giản, không phải transaction nhiều bước, và xác suất thất bại thấp. Đó là tiêu chí để quyết định có nên dùng Optimistic Update không.
Lưu ý quan trọng
Chỉ dùng cho thao tác đơn giản
Optimistic Update phù hợp với các action nhẹ như toggle, increment, reorder. Đừng áp dụng cho thanh toán, xác nhận đơn hàng, hay bất kỳ flow nào mà lỗi sẽ gây hậu quả nghiêm trọng — những trường hợp đó cần feedback chính xác từ server.
Luôn xử lý lỗi phía server
React tự rollback UI khi Server Action throw error, nhưng bạn vẫn nên thông báo cho người dùng biết thao tác thất bại. Kết hợp với useActionState hoặc toast notification để hiển thị lỗi.
Không mutate state trực tiếp trong reducer
Trong reducer của useOptimistic, luôn tạo bản sao object/array trước khi sửa ({ ...post }, [...posts]). Mutate trực tiếp sẽ gây bug khó debug.
useOptimistic chỉ hoạt động trong async transition
Hook này được thiết kế để dùng bên trong startTransition hoặc Server Action. Nếu bạn thấy state optimistic không tự rollback, kiểm tra lại xem await có đúng chỗ không.
Tóm tắt
Optimistic Update là một trong những kỹ thuật tạo ra cảm giác ứng dụng “nhanh” mà không cần tối ưu gì thêm phía backend. Chỉ cần áp dụng đúng chỗ — những thao tác nhẹ, xác suất lỗi thấp — là đủ để trải nghiệm người dùng tốt hơn đáng kể.