Tác động tâm lý tiêu cực của biểu tượng Loading
Tất cả chúng ta đều từng cảm thấy sự ức chế nhỏ mỗi khi nhấn nút “Thích” hoặc gửi tin nhắn mà trong tích tắc không có gì xảy ra. Sau đó là một vòng xoay màu xám bắt đầu quay. Cuối cùng, sau khoảng 500ms phản hồi từ server, giao diện mới cập nhật. Sự chậm trễ này khiến ngay cả những ứng dụng hiện đại nhất, bao gồm cả các dự án phát triển ứng dụng Desktop, cũng trở nên chậm chạp và nặng nề.
Điều này xảy ra bởi các mô hình web truyền thống thường đợi xác nhận từ server trước khi hiển thị thay đổi. Với kết nối 4G chập chờn, thời gian chờ đợi có thể dễ dàng tăng lên hơn một giây. Nghiên cứu cho thấy người dùng có thể nhận ra độ trễ chỉ từ 100ms. Nếu giao diện không phản hồi ngay lập tức, họ sẽ cảm thấy ứng dụng như bị lỗi.
Trước React 19, việc xây dựng “Optimistic UI” (Giao diện lạc quan) — kỹ thuật hiển thị trạng thái thành công trước khi server thực sự xử lý xong — là một bài toán hóc búa. Bạn phải theo dõi trạng thái cũ một cách thủ công, viết logic xử lý lỗi phức tạp và kích hoạt rollback (hoàn tác) nếu API thất bại. Đó là công thức tạo ra code “mì tôm” (spaghetti code) và các lỗi đồng bộ hóa.
Bắt đầu nhanh: Bản cập nhật lạc quan đầu tiên của bạn
Hook useOptimistic mới giúp loại bỏ những đoạn code rườm rà đó. Nó cho phép bạn định nghĩa một trạng thái tạm thời chỉ tồn tại trong khi một hành động bất đồng bộ đang được thực hiện.
Hãy xem xét một hộp tin nhắn đơn giản. Thay vì đợi ghi vào cơ sở dữ liệu, chúng ta có thể hiển thị tin nhắn lên màn hình ngay lập tức.
import { useOptimistic, useRef } from 'react';
function MessageBox({ messages, sendMessage }) {
const formRef = useRef();
// 1. Định nghĩa trạng thái lạc quan
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [...state, { text: newMessage, sending: true }]
);
async function formAction(formData) {
const message = formData.get("message");
// 2. Cập nhật giao diện ngay lập tức
addOptimisticMessage(message);
formRef.current.reset();
// 3. Kích hoạt yêu cầu server thực tế
await sendMessage(message);
}
return (
<>
<div>
{optimisticMessages.map((m, i) => (
<div key={i} style={{ opacity: m.sending ? 0.6 : 1 }}>
{m.text} {m.sending && <small>(Đang gửi...)</small>}
</div>
))}
</div>
<form action={formAction} ref={formRef}>
<input type="text" name="message" placeholder="Nhập tin nhắn..." />
<button type="submit">Gửi</button>
</form>
</>
);
}
React 19 quản lý vòng đời tại đây. Khi bạn gọi addOptimisticMessage, giao diện sẽ cập nhật tức thì. Khi hành động sendMessage hoàn tất, React sẽ tự động thay thế trạng thái tạm thời bằng dữ liệu thực tế từ server.
Cơ chế hoạt động bên dưới của useOptimistic
Hook này dựa trên hệ thống transition của React. Khi bạn kích hoạt một cập nhật bên trong một transition — như Server Action — React sẽ theo dõi trạng thái “đang xử lý” (pending). Hook này yêu cầu hai phần cụ thể:
- Trạng thái truyền vào (Passthrough state): Nguồn dữ liệu gốc (thường là props hoặc state từ component cha).
- Hàm Reducer: Một hàm thuần túy (pure function) để gộp dữ liệu mới với trạng thái hiện tại.
Ưu điểm lớn nhất là khả năng tự động rollback. Nếu API trả về lỗi 500, bạn không cần viết hàm “hoàn tác”. Vì trạng thái lạc quan gắn liền với vòng đời của hành động bất đồng bộ, React chỉ đơn giản là loại bỏ phiên bản tạm thời và quay lại trạng thái xác thực cuối cùng ngay khi hành động kết thúc.
Gần đây tôi đã áp dụng điều này vào một dự án mà người dùng phàn nàn về việc hoàn thành tác vụ bị “lag”. Bằng cách chuyển sang useOptimistic, giao diện mang lại cảm giác nhanh gấp đôi. Thời gian phản hồi của API không thay đổi, nhưng cảm nhận về tốc độ của người dùng đã thay đổi hoàn toàn, tương tự như khi làm chủ Vite để tối ưu hóa hiệu năng.
Sự kết hợp hoàn hảo với Server Actions
Mặc dù tương thích với các trình xử lý sự kiện tiêu chuẩn, hook này hoạt động tốt nhất với Server Actions, giúp loại bỏ boilerplate với React 19 Actions. Khi người dùng gửi form, React sẽ tự động bắt đầu một transition. Hook useOptimistic nhận biết transition này và áp dụng các thay đổi giao diện tạm thời cho đến khi server phản hồi.
Các mẫu nâng cao: Không chỉ là chuỗi ký tự đơn giản
Trong thực tế, hiếm khi bạn chỉ đẩy một chuỗi vào mảng. Bạn thường cần quản lý các ID tạm thời hoặc định dạng riêng cho các mục đang “chờ xử lý”.
const [optimisticItems, addOptimisticItem] = useOptimistic(
items,
(state, newItem) => [
...state,
{
...newItem,
id: crypto.randomUUID(), // Ngăn chặn trùng lặp key
isPending: true
}
]
);
Sử dụng cờ isPending cho phép bạn thay đổi giao diện của mục đó. Bạn có thể làm nó mờ đi hoặc thêm biểu tượng “Đang đồng bộ”. Điều này thông báo cho người dùng rằng hành động của họ đã được ghi nhận mà không bắt họ phải đợi xác nhận thành công.
Xử lý lỗi một cách tinh tế
Khi một yêu cầu server thất bại, useOptimistic sẽ xử lý việc hoàn tác giao diện, nhưng bạn vẫn cần thông báo cho người dùng. Kết hợp hook này với hệ thống thông báo (toast) và các kỹ thuật mở rộng TypeScript để quản lý lỗi sẽ mang lại trải nghiệm tốt nhất.
async function handleAction(formData) {
try {
addOptimisticItem({ name: formData.get('name') });
await updateDatabase(formData);
} catch (error) {
toast.error("Mất kết nối. Các thay đổi không được lưu.");
// React tự động xử lý việc rollback giao diện tại đây
}
}
Các quy tắc tốt nhất để có trải nghiệm mượt mà
Cập nhật lạc quan rất mạnh mẽ, nhưng không phải là giải pháp cho mọi trường hợp. Hãy sử dụng ba quy tắc sau để giữ cho UX luôn nhất quán:
1. Tránh các thao tác quan trọng
Không bao giờ sử dụng useOptimistic cho các hành động quan trọng về tài chính hoặc bảo mật. Nếu người dùng đang chuyển 5.000 USD giữa các tài khoản, họ cần biết chắc chắn rằng server đã xác nhận giao dịch. Việc hiển thị thông báo “Thành công” rồi đột ngột biến mất do lỗi mạng sẽ phá hủy niềm tin của người dùng.
2. Giữ cho logic reducer luôn thuần túy
Hàm reducer chỉ được phép tính toán trạng thái tiếp theo. Đừng kích hoạt các hiệu ứng lề (side effects), gọi API hoặc phân tích dữ liệu bên trong hàm này. Nó phải là một phép chuyển đổi dữ liệu có thể dự đoán được từ dữ liệu hiện có.
3. Mô phỏng hành vi của server
Cập nhật lạc quan của bạn nên mô phỏng logic của server. Nếu backend của bạn sắp xếp các bình luận mới nhất lên đầu, thì reducer lạc quan cũng nên chèn tin nhắn vào đầu danh sách. Nếu giao diện bị nhảy hoặc tự sắp xếp lại khi dữ liệu thực tế trả về, quá trình chuyển đổi sẽ gây cảm giác khó chịu thay vì mượt mà.
Tổng kết
Hook useOptimistic của React 19 là một bản nâng cấp đáng giá cho các nhà phát triển frontend. Nó biến một công việc quản lý trạng thái phức tạp thành một mô hình khai báo (declarative) dễ quản lý. Bằng cách dự đoán tương lai, bạn có thể xây dựng các giao diện mang lại cảm giác tức thì, che giấu độ trễ vốn có của internet một cách hiệu quả, nhất là khi cần mở rộng React Navigation trong các hệ thống lớn.
Lần tới khi bạn xây dựng một tính năng cần phản hồi từ server — như phần bình luận hoặc nút bật/tắt cài đặt — hãy thử thay thế vòng xoay loading đó. Người dùng của bạn chắc chắn sẽ đánh giá cao tốc độ vượt trội này.

