Vấn đề với cách làm thông thường
Cách đơn giản nhất để implement tính năng này là: gọi Server Action, đợi response, rồi mới cập nhật UI.useOptimistic là gì?
useOptimistic là một hook được React giới thiệu từ phiên bản 19, cho phép bạn hiển thị một giá trị tạm thời ngay lập tức, trong khi async operation vẫn đang chạy ở background. Khi operation hoàn thành (thành công hoặc thất bại), React tự động đồng bộ lại với giá trị thực.
Hook này được cấu hình như sau:
optimisticValuelà giá trị bạn hiển thị lên UI — có thể là giá trị tạm thời, hoặc giá trị thực nếu không có operation nào đang chạy.addOptimistic(next)kích hoạt cập nhật UI ngay lập tức với giá trị tạm thời.- Nếu server thất bại, React tự động rollback
optimisticValuevềrealValue. Bạn không cần tự xử lý rollback. - Nếu server thành công, bạn cần tự cập nhật
realValueđể “confirm” kết quả.
useOptimistic phải chạy bên trong useTransition (hoặc một Server Action). Đây là yêu cầu bắt buộc — hook này được thiết kế để hoạt động trong React’s transition system.
Refactor từng bước
Bước 1 — Tách “giá trị đã xác nhận” ra khỏi render state
Trước đây,useState vừa là nơi lưu giá trị server trả về, vừa là nguồn dữ liệu để render. Bây giờ chúng ta cần tách chúng ra:
savedReaction đóng vai trò là source of truth từ server. Chúng ta chỉ cập nhật nó khi server xác nhận thành công.
Bước 2 — Khai báo useOptimistic
reducer) nhận vào currentReaction (giá trị hiện tại) và nextMood (giá trị bạn muốn thay đổi thành), rồi tính toán và trả về state tạm thời. Ở đây, helper getNextReactionCount xử lý logic tăng/giảm count:
Bước 3 — Sử dụng addOptimisticReaction với useTransition
Sử dụng [optimisticReaction, addOptimisticReaction] được trả ra từ hook useOptimistic để cập nhật lại handler.
Các hàm của useOptimistic yêu cầu chạy trong một transition. Thêm useTransition và bọc toàn bộ async logic:
optimisticReaction. Khi transition kết thúc mà không có lời gọi setSavedReaction, React tự động trả optimisticReaction về giá trị của savedReaction. Đó là toàn bộ cơ chế rollback.
Bước 4 — Render từ optimisticReaction để cập nhật UI “lạc quan”
Chỗ này bạn cần hiểu làoptimisticReaction là giá trị trả về từ hàm reducer (currentReaction, nextMood: MoodSelection) bên trong useOptimistic hook, cụ thể hơn:
- Khi không có transition nào đang chạy:
optimisticReaction === savedReaction, nghĩa React trả thẳng giá trị gốc, hàm reducer không được gọi. - Khi đang trong transition (sau khi
addOptimisticReaction(nextMood)được gọi): React gọi hàm reducer với(savedReaction, nextMood), và kết quả trả về chính làoptimisticReactionlúc đó.
optimisticReaction là React tự quyết định trả về cái gì tùy theo trạng thái hiện tại — giá trị gốc hoặc giá trị tạm thời.
Rồi, giờ xem lại phần render của cách làm thông thường — state đến thẳng từ useState, button không có trạng thái chờ:
optimisticReaction thay vì trực tiếp từ useState, button có thêm disabled={isPending} (do useTransition trả ra) để tránh double-click, và có thêm một chỗ hiển thị lỗi nếu server thất bại:
Kết quả: timeline của một lần click
| Thời điểm | Điều xảy ra |
|---|---|
| Click | addOptimisticReaction(nextMood) → UI cập nhật ngay |
| ~0ms | optimisticReaction phản chiếu mood mới, count mới |
| ~200ms | Server xử lý xong, trả về kết quả |
| Thành công | setSavedReaction(...) → optimisticReaction đồng bộ với server |
| Thất bại | Transition kết thúc → optimisticReaction tự rollback về savedReaction |
Một vài lưu ý thực tế
VềuseTransition bắt buộc: Nếu bạn gọi addOptimisticReaction bên ngoài startTransition, React sẽ báo lỗi ở development mode. Đây là thiết kế có chủ ý — useOptimistic chỉ có nghĩa khi có một async transition đang diễn ra để nó “che” đi.
Về isPending: isPending là true trong suốt thời gian transition, bao gồm cả sau khi optimistic update đã hiển thị. Dùng nó để disable input, hiện loading indicator nhỏ, hoặc giảm opacity — nhưng không nên dùng để ẩn nội dung đã optimistic, vì như vậy sẽ phá vỡ mục đích ban đầu.
Về rollback: Rollback tự động hoạt động tốt cho các thao tác đơn giản. Nếu UI phức tạp hơn (ví dụ: optimistic insert một item vào list), bạn cần đảm bảo savedReaction (giá trị gốc) không bao gồm item chưa được xác nhận — nếu không, rollback sẽ không về đúng trạng thái cũ.
Giả sử bạn có một comment list, và người dùng submit một comment mới.
Làm sai — thêm comment vào savedList trước:
savedList khi server xác nhận:
savedList là “bức ảnh đã được server xác nhận”, useOptimistic chỉ layer lên trên nó. Nếu bạn làm bẩn savedList trước khi server confirm thì rollback không còn chỗ sạch để về nữa. Nhưng chắc bạn sẽ không “ngốc” vậy đâu nhỉ, ahihi…
Tóm lại
useOptimistic giải quyết một vấn đề UX rất cụ thể: khoảng trống giữa lúc người dùng tương tác và lúc server phản hồi. Bằng cách hiển thị giá trị tạm thời ngay lập tức và tự rollback khi có lỗi, hook này giúp UI cảm giác nhanh và responsive mà không cần bạn tự quản lý state rollback phức tạp.
Pattern cốt lõi gồm 3 phần: useState giữ giá trị đã xác nhận, useOptimistic tạo giá trị hiển thị tạm thời, và useTransition bọc toàn bộ async flow lại.
Nếu bạn muốn tìm hiểu thêm, một số chủ đề liên quan đáng đọc tiếp là useActionState (kết hợp form action với optimistic update), revalidatePath / revalidateTag trong Next.js (để đồng bộ cache sau khi mutation), và Suspense boundaries (để kiểm soát loading state ở cấp độ cao hơn).