useTransition cho phép bạn đánh dấu một lần cập nhật state là không khẩn cấp. React render lần cập nhật đó ở chế độ nền, rồi bỏ đi hoặc bắt đầu lại nếu có việc khẩn cấp hơn xảy ra, như một cú bấm hay một lần gõ phím, nhờ vậy trang vẫn phản hồi trong khi phần chậm của màn hình dần bắt kịp.
Bấm posts, rồi trước khi danh sách hiện ra, bấm contact. Contact hiện ra ngay: React đã bỏ lần render posts còn dang dở. Giờ hãy đổi startTransition(() => { setTab(next); }) thành setTab(next); thông thường rồi thử lại: trang bị đứng khoảng 300 ms sau khi bấm posts, và cú bấm vào contact phải chờ cho đến khi posts xong.
Cú pháp
const [isPending, startTransition] = useTransition();
startTransition(() => {
setTab(next);
});
startTransitionnhận một hàm và chạy nó ngay lập tức. Mọi lần cập nhật state bên trong hàm đó đều được đánh dấu là transition.isPendingcó giá trịtruetừ lúc bạn bắt đầu transition cho đến khi React render xong và commit kết quả.
useTransition không nhận đối số nào. Hàm bạn truyền vào chạy đồng bộ; transition nói về cách React render lần cập nhật sinh ra từ hàm đó, chứ không phải về thời điểm hàm của bạn chạy.
"Không khẩn cấp" nghĩa là gì
React chia các lần cập nhật thành hai loại. Cập nhật khẩn cấp, như gõ phím, bấm chuột và nhấn nút, cần phản ánh ngay lập tức, nếu không trang sẽ có cảm giác bị hỏng. Cập nhật transition, như chuyển sang một giao diện mới hay hiển thị kết quả đã lọc, có thể mất một chút thời gian.
React render một transition theo từng phần nhỏ và kiểm tra giữa các component xem có việc gì khẩn cấp vừa đến hay không. Nếu có, React tạm dừng hoặc bỏ transition, xử lý lần cập nhật khẩn cấp, rồi bắt đầu lại transition với state mới nhất. Trong lúc một transition đang render, màn hình cũ vẫn được giữ nguyên; React không hiển thị kết quả mới dựng được một nửa. Đó là lý do ví dụ trên vẫn hiển thị nội dung about, với dòng Loading bên cạnh các nút, cho đến khi posts sẵn sàng.
Việc ngắt diễn ra giữa các component. Một component duy nhất chặn suốt 300 ms thì không thể bị cắt làm đôi, vì vậy phần việc chậm ở trên được chia ra cho 300 component SlowPost nhỏ.
Hiển thị trạng thái đang chờ
isPending là chỗ để đặt chỉ báo đang tải mà không thay thế nội dung hiện tại: một vòng xoay cạnh nút bấm, chữ mờ đi, một nhãn "Saving...". Trong ví dụ đầu tiên, nó hiển thị Loading... bên cạnh các tab. Bạn cũng có thể dùng nó để tạo kiểu cho nội dung:
<div style={{ opacity: isPending ? 0.6 : 1 }}>{content}</div>
Lọc một danh sách lớn trong khi gõ
Bạn không thể đặt chính giá trị của ô nhập vào một transition. Một <input> được kiểm soát phải nhận giá trị mới trong cùng lần cập nhật khẩn cấp, nếu không các ký tự sẽ bị mất. Thay vào đó, hãy giữ hai phần state: văn bản trong ô nhập (khẩn cấp) và truy vấn mà danh sách dùng (một transition).
Gõ 123 thật nhanh. Ô nhập không bao giờ bị trễ. Console vẫn ghi một dòng rendering results cho mỗi lần gõ phím, vì Results ghi log ngay khi React bắt đầu render nó, nhưng danh sách cho 1 và 12 không bao giờ lên đến màn hình: mỗi lần gõ phím mới hơn đã ngắt lần render đang diễn ra, và React bắt đầu lại với truy vấn mới nhất. Results được bọc trong memo là có chủ ý: nếu không có nó, lần render khẩn cấp cho mỗi lần gõ phím cũng sẽ render danh sách chậm với truy vấn cũ, và bạn mất đi lợi ích của transition.
startTransition không cần hook
react cũng export startTransition dưới dạng một hàm bình thường. Hàm này đánh dấu các lần cập nhật là transition y như hook, nhưng bạn không có isPending. Hãy dùng nó ở những chỗ không thể gọi hook, ví dụ trong một thư viện dữ liệu hoặc một router nằm ngoài mọi component.
import { startTransition } from 'react';
function navigate(url) {
startTransition(() => {
setCurrentUrl(url);
});
}
Transition bất đồng bộ trong React 19
Kể từ React 19, hàm bạn truyền vào có thể là async. Khi đó isPending giữ giá trị true trong suốt thời gian hàm chạy, kể cả lúc chờ, nên đây là cách đơn giản để hiển thị trạng thái đang chờ trong một request. Hàm giả saveName bên dưới đóng vai một request thật.
Hãy để ý lời gọi startTransition thứ hai bọc quanh setSaved. React mất dấu transition khi đi qua một await, nên một lần cập nhật state sau đó phải được bọc lại thì mới tính là một phần của transition. React gọi một hàm được truyền theo cách này là action; useActionState và form action đều được xây dựng trên cùng cơ chế này.
Transition không phải là debounce
Debounce chờ một khoảng thời gian cố định sau lần gõ phím cuối cùng rồi mới làm gì đó. Transition thì không chờ. Nó bắt đầu render ngay lập tức, và nếu có lần cập nhật mới hơn đến, nó bỏ phần việc cũ. Trên thiết bị nhanh, kết quả hiện ra không hề có độ trễ; trên thiết bị chậm, React làm được nhiều nhất có thể giữa các lần gõ phím. Điều mà transition không làm được là giảm số lượng request mạng: nếu mỗi lần gõ phím đều bắt đầu một lần fetch, hãy debounce phần fetch.
Transition và Suspense
Transition cũng thay đổi cách một boundary Suspense hoạt động. Bình thường, khi một component bên trong boundary bị suspend (một component lazy đang tải, hoặc dữ liệu được đọc bằng use), React thay nội dung bằng fallback. Nếu lần cập nhật gây ra việc suspend là một transition, React sẽ giữ nội dung đang có trên màn hình, và isPending giữ giá trị true cho đến khi nội dung mới sẵn sàng. Đây là cách các router tránh việc nháy một vòng xoay to bằng cả trang ở mỗi lần điều hướng: chúng bọc việc điều hướng trong một transition, và trang cũ vẫn hiển thị cho đến khi trang tiếp theo render được. Nội dung chưa từng được hiển thị vẫn dùng fallback, nên một boundary mới xuất hiện vẫn hoạt động như bình thường.
Những trường hợp không hoạt động
- Cập nhật bên trong timeout.
startTransition(() => setTimeout(() => setTab(next), 100))không đánh dấu lần cập nhật, vì nó chạy sau khistartTransitionđã trả về. Hãy đặtstartTransitionbên trong timeout. - Một component chậm duy nhất. React chỉ có thể ngắt giữa các component, nên một component chặn trong thời gian dài vẫn sẽ chặn.
- Kiểm soát ô nhập. Như đã nói ở trên, state của chính ô nhập vẫn phải khẩn cấp.
Với trường hợp bạn không sở hữu hàm setter của state mà chỉ nhận được một giá trị, chẳng hạn một prop, useDeferredValue cho bạn hành vi tương tự.
Câu hỏi thường gặp
useTransition dùng để làm gì?
Hook này cho bạn startTransition, đánh dấu các lần cập nhật state bên trong nó là không khẩn cấp, và isPending, có giá trị true khi lần cập nhật đó vẫn đang render. React có thể ngắt một lần render không khẩn cấp để xử lý cú bấm và thao tác gõ phím trước.
useTransition và startTransition khác nhau thế nào?
Cả hai đều đánh dấu các lần cập nhật là transition. Hook useTransition còn cho bạn thêm isPending để hiển thị trạng thái đang tải. Hàm startTransition độc lập import từ react dùng được bên ngoài component nhưng không có cờ pending.
Có thể dùng useTransition cho ô nhập văn bản không?
Không dùng cho chính giá trị của ô nhập. Một input được kiểm soát (controlled input) phải cập nhật đồng bộ, nếu không việc gõ phím sẽ bị lỗi. Hãy giữ state của ô nhập ở dạng khẩn cấp và đặt phần cập nhật tốn kém phụ thuộc vào nó trong một transition, hoặc dùng useDeferredValue.
useTransition có giống debounce không?
Không. Debounce chờ một khoảng thời gian cố định rồi mới làm gì đó. Transition bắt đầu render ngay lập tức và bị ngắt nếu có lần cập nhật mới hơn, nên trên thiết bị nhanh, kết quả hiện ra không hề có độ trễ.
startTransition có nhận hàm async không?
Có, kể từ React 19. isPending giữ giá trị true cho đến khi hàm async chạy xong. Các lần cập nhật state sau một await phải được bọc trong một lời gọi startTransition khác thì mới được đánh dấu là transition.