Menu

Hook useOptimistic trong React: UI tức thì, tự hoàn tác

useOptimistic hiện kết quả bạn mong đợi trong lúc một action async vẫn đang chạy, rồi quay về state thật khi action kết thúc, nên một request thất bại sẽ tự hoàn tác. Tìm hiểu cú pháp, dạng reducer và vì sao nó phải chạy bên trong một action.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

useOptimistic là một hook của React 19 cho phép màn hình hiện kết quả của một action trước khi action đó xong. Bạn đặt một giá trị optimistic bên trong action; React hiện nó trong lúc action đang chờ, rồi quay về state thật. Nếu request thất bại, state thật chưa từng thay đổi, nên màn hình tự hoàn tác.

API giả trong ví dụ này đóng vai một request thật và thất bại cứ mỗi lần gọi thứ ba, để bạn thấy được cả hai kết quả.

Click nút ba lần, mỗi lần cách nhau một giây. Hai lần đầu đổi ngay lập tức và dòng đã lưu theo kịp sau một giây. Lần thứ ba cũng đổi ngay, rồi lật lại khi request thất bại. Đổi calls % 3 thành calls % 2 thì cứ hai lần click lại có một lần thất bại.

Cú pháp

const [optimisticState, setOptimistic] = useOptimistic(state, updateFn?);
  • state là giá trị thật, thường lấy từ useState, props hoặc useActionState. Khi không có action nào đang chờ, optimisticState chính là giá trị này.
  • setOptimistic(value) đặt giá trị optimistic trong suốt thời gian action hiện tại chạy.
  • updateFn là không bắt buộc: (currentState, optimisticValue) => nextState. Có nó, setOptimistic nhận một phần thay đổi thay vì cả một giá trị mới. Xem dạng reducer bên dưới.

Vòng đời của một lần click luôn giống nhau:

  1. Bên trong một action, bạn gọi setOptimistic. Màn hình cập nhật ngay.
  2. Action chờ request.
  3. Nếu thành công, bạn cập nhật state thật (bên trong startTransition khi việc đó nằm sau một await).
  4. Action kết thúc. optimisticState lại bám theo state: giá trị đã lưu nếu thành công, giá trị cũ nếu thất bại.

Bước 4 là lý do ví dụ không có code hoàn tác nào. Khối catch chỉ đặt một thông báo lỗi.

Phải chạy bên trong một action

Giá trị optimistic chỉ tồn tại khi một transition đang chờ, nên setOptimistic phải được gọi bên trong một transition. Tất cả những chỗ sau đều được tính:

  • hàm bạn truyền cho startTransition (hoặc startTransition lấy từ useTransition),
  • hàm truyền cho <form action={...}> hoặc <button formAction={...}>,
  • action bạn đưa cho useActionState.

Nếu gọi từ một onClick thông thường, giá trị optimistic không có action nào để tồn tại bên trong. React hoàn tác nó ngay, và trong môi trường phát triển React ghi log "An optimistic state update occurred outside a transition or action".

Các cập nhật state thật diễn ra sau một await nên được bọc trong startTransition riêng, như startTransition(() => setLiked(next)) ở trên. Sau một await, React không thể biết bạn vẫn đang ở trong transition trước đó.

Danh sách tin nhắn với dạng reducer

Khi state optimistic là một danh sách, hãy truyền một hàm cập nhật làm đối số thứ hai. Khi đó setter nhận phần tử mới, và React nối nó vào danh sách thật tại thời điểm đó.

Gửi lần lượt ba tin nhắn với nội dung khác nhau. Mỗi tin hiện mờ kèm "(sending...)" và rõ nét khi đã gửi xong; tin thứ ba biến khỏi khung chat và hiện thành "Not sent". Nếu gửi thật nhanh, cả ba vẫn mờ cho đến khi request cuối cùng kết thúc, vì React giữ các giá trị optimistic cho đến khi mọi action đang chờ đã xong. Form action vốn đã là một transition, nên ở đây addOptimistic không cần startTransition riêng.

Cho mỗi phần tử optimistic một key không trùng với phần tử thật. Ví dụ dùng pending- cộng với nội dung tin, nên gửi cùng một nội dung hai lần khi cả hai đều đang chờ sẽ bị trùng; ứng dụng thật sẽ tạo id ở phía client.

Nhiều cập nhật cùng lúc

Vì hàm cập nhật nhận state hiện tại, các cập nhật optimistic chồng lên nhau. Click nhanh thì mỗi lần click đang chờ được áp lên trên lần trước.

Click nhanh bốn lần. Giỏ hàng hiện 4 ngay lập tức. Khi các request xong, con số dừng ở 3, vì request thứ ba thất bại và console có ghi lại. React giữ các giá trị optimistic cho đến khi mọi action đang chờ đã xong, rồi hiện số lượng thật.

Khi nào nên dùng, khi nào không

Cập nhật optimistic hợp với những action gần như luôn thành công và dễ hoàn tác: thích, gắn sao, bật tắt, đổi tên, thêm tin nhắn. Trong trường hợp thường gặp, người dùng không phải thấy biểu tượng đang tải.

Tránh dùng chúng ở chỗ một trạng thái "đã xong" giả sẽ gây hiểu lầm: thanh toán, xóa tài khoản, bất cứ thứ gì người dùng có thể hành động dựa vào trước khi biết kết quả. Với những trường hợp đó, hãy hiện trạng thái đang chờ bằng isPending từ useActionState hoặc useTransition và đợi câu trả lời thật.

Luôn báo cho người dùng khi có hoàn tác. Một giá trị lặng lẽ đổi lại trông giống bug. Cả hai ví dụ trên giữ thông báo lỗi trong state thông thường để nó tồn tại lâu hơn action.

Kết hợp với useActionState

useOptimistic và useActionState đi cùng nhau rất hợp. Action từ useActionState vốn đã là một transition, nên bạn có thể đặt giá trị optimistic ngay đầu action, và state trả về là giá trị thật mà giá trị optimistic quay về.

const [state, formAction] = useActionState(async (previous, formData) => {
    const title = formData.get('title');
    setOptimisticTitle(title);
    const saved = await saveTitle(title);
    return { title: saved.title };
}, { title: 'Untitled' });

const [optimisticTitle, setOptimisticTitle] = useOptimistic(state.title);

Trong lúc lưu, trang hiện tiêu đề mới. Khi xong, state.title chứa giá trị đã lưu. Nếu saveTitle ném lỗi, lỗi của action sẽ đi tới error boundary gần nhất; để hiện lại tiêu đề cũ kèm thông báo, hãy bắt lỗi bên trong action và trả về state trước đó cùng với lỗi.

Lỗi thường gặp

Gọi setter bên ngoài action. Giá trị optimistic không ở lại trên màn hình, và React cảnh báo trong môi trường phát triển. Bọc nó trong startTransition hoặc chuyển nó vào một form action.

Quên cập nhật state thật. Khi thành công, action phải thay đổi state mà useOptimistic phản chiếu (setLiked, setMessages). Nếu không, giá trị optimistic biến mất khi action kết thúc, và một request thành công trông như bị hoàn tác.

Cập nhật state thật sau await mà không có transition. Cập nhật vẫn được áp dụng, nhưng React có thể hiện nó vào một thời điểm khác với lúc action kết thúc. Hãy bọc nó trong startTransition, như cả ba ví dụ đều làm.

Dùng giá trị optimistic làm nguồn dữ liệu chính. Gửi request và tính tổng từ state thật. Giá trị optimistic chỉ để hiển thị và có thể bị bỏ đi bất cứ lúc nào.

Câu hỏi thường gặp

useOptimistic dùng để làm gì?

Nó cho bạn một bản sao của một phần state mà bạn có thể thay đổi ngay trong lúc một action async đang chạy. Khi action kết thúc, bản sao quay lại bám theo state thật, lúc đó chứa hoặc kết quả đã lưu, hoặc giá trị cũ.

useOptimistic hoàn tác khi lỗi như thế nào?

Nó không cần một nhánh xử lý lỗi riêng. Giá trị optimistic chỉ tồn tại trong lúc action đang chờ. Nếu request thất bại và bạn không cập nhật state thật, hook sẽ hiện lại state thật, tức là giá trị trước lúc click.

Vì sao gặp lỗi "An optimistic state update occurred outside a transition or action"?

Hàm setter được gọi từ một event handler thông thường. Hãy gọi nó bên trong hàm truyền cho startTransition, bên trong một <form action>, hoặc bên trong action từ useActionState.

Đối số thứ hai của useOptimistic là gì?

Một hàm cập nhật không bắt buộc, (currentState, optimisticValue) => newState, giống một reducer. Có nó, setter chỉ nhận phần thay đổi (một tin nhắn mới, +1) và React tự tính state optimistic, kể cả khi có nhiều cập nhật đang chờ cùng lúc.

useOptimistic chỉ dùng cho form thôi sao?

Không. Nó hoạt động trong mọi transition, nên một nút gọi startTransition(async () => { ... }) cũng dùng được, không chỉ form action.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu