useDeferredValue cho bạn một bản sao của giá trị được phép chậm lại phía sau. Khi giá trị thay đổi, React vẽ lại màn hình trước với bản sao cũ, giúp việc gõ phím vẫn tức thì, rồi render phần chậm với giá trị mới ở chế độ nền.
Gõ nhanh một từ. Ô nhập vẫn theo kịp, còn danh sách mờ đi khi nó đang chậm hơn. Console ghi một dòng SlowList rendering cho mỗi chữ cái, vì SlowList ghi log ngay khi React bắt đầu render nó, nhưng danh sách cho các giá trị trung gian không bao giờ lên tới màn hình: mỗi chữ cái mới ngắt lần render nền, và React bắt đầu lại với văn bản mới nhất. Đổi query={deferredQuery} thành query={query} rồi gõ lại: giờ mỗi lần gõ phím phải chờ danh sách một phần tư giây.
Cú pháp
const deferredValue = useDeferredValue(value);
const deferredValue = useDeferredValue(value, initialValue); // React 19
valuelà bất cứ thứ gì bạn render: một chuỗi từ ô nhập, một con số, một object. Kiểu nguyên thủy hoạt động tốt nhất, vì một object mới ở mỗi lần render luôn được tính là một thay đổi.- Giá trị trả về phần lớn thời gian bằng
value. Ngay sau khivaluethay đổi, nó vẫn giữ giá trị trước đó trong một lần render, rồi bắt kịp trong một lần render nền.
Cách nó hoạt động
Mỗi thay đổi tạo ra hai lần render:
- Một lần render khẩn cấp với
valuemới vàdeferredValuecũ. Ô nhập hiển thị văn bản mới. Các component chỉ nhậndeferredValuenhận đúng props như trước. - Một lần render nền với
deferredValuemới. Đây là lần render chậm. React render nó theo từng phần nhỏ và bỏ dở nếuvaluelại thay đổi trước khi nó xong, rồi bắt đầu lại với giá trị mới nhất.
Bạn có thể quan sát điều này bằng cách ghi log cả hai giá trị:
Gõ một chữ cái, chẳng hạn a, và Console in thêm hai dòng: đầu tiên là text = "a" với deferred = "", sau đó cả hai đều là "a". Lần render đầu đó là thứ giữ cho việc gõ phím nhanh, nhưng chỉ khi phần chậm có thể bỏ qua nó. Đó là lý do SlowList trong ví dụ đầu tiên được bọc trong memo: trong lần render khẩn cấp, prop query của nó chưa thay đổi, nên React bỏ qua nó. Không có memo, lần render khẩn cấp vẫn chạy toàn bộ danh sách chậm, chỉ là với văn bản cũ, và bạn chẳng được lợi gì.
Hiển thị nội dung cũ
Trong lúc lần render nền đang diễn ra, value và giá trị trì hoãn khác nhau. So sánh chúng để báo cho người dùng biết nội dung đã cũ, mà không phải ẩn nó đi:
const isStale = query !== deferredQuery;
<div style={{ opacity: isStale ? 0.5 : 1, transition: 'opacity 0.2s' }}>
<SlowList query={deferredQuery} />
</div>
Cách này thường dễ chịu hơn một biểu tượng loading: kết quả cũ vẫn đọc được và được thay ngay tại chỗ khi kết quả mới sẵn sàng. Ý tưởng tương tự cũng áp dụng với Suspense: nếu giá trị trì hoãn khiến một component tạm dừng trong lúc tải dữ liệu, React tiếp tục hiển thị nội dung cũ thay vì fallback cho đến khi nội dung mới sẵn sàng (xem Suspense và lazy).
Giá trị ban đầu (React 19)
Theo mặc định, lần render đầu tiên không có gì để chậm theo, nên giá trị trì hoãn là giá trị thật ngay từ đầu. Từ React 19, bạn có thể truyền đối số thứ hai. Ở lần render đầu, hook trả về initialValue, sau đó React render lại ở chế độ nền với giá trị thật. Nhờ vậy bạn có thể hiển thị ngay một thứ nhẹ nhàng và điền phần nặng vào ngay sau đó.
Console hiển thị hai lần render: lần đầu với "", sau đó với toàn bộ văn bản. Bỏ đối số thứ hai và chỉ một lần render được ghi log, với toàn bộ văn bản ngay lập tức.
useDeferredValue và useTransition
Cả hai đều đánh dấu công việc là không khẩn cấp và dùng chung cơ chế render. Khác biệt nằm ở chỗ bạn đặt chúng:
- useTransition bọc lệnh cập nhật. Bạn gọi
startTransition(() => setQuery(text)), nên bạn cần có hàm set. Bạn nhận đượcisPendingmà không phải làm gì thêm. - useDeferredValue bọc giá trị. Bạn gọi nó ở nơi giá trị được dùng, nên nó hoạt động khi giá trị đến dưới dạng prop hoặc từ một hook bạn không kiểm soát. Bạn tự tính trạng thái "đang chờ" bằng
value !== deferredValue.
// You own the state: either works, a transition is direct
startTransition(() => setQuery(nextQuery));
// You only receive the value
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
return <SlowList query={deferredQuery} />;
}
useDeferredValue và debounce, throttle
Debounce chờ một khoảng thời gian cố định sau lần thay đổi cuối cùng; throttle cập nhật nhiều nhất một lần trong mỗi khoảng. Cả hai đều chọn trước một độ trễ, nên trên một laptop nhanh chúng khiến trang chậm hơn mức cần thiết, còn trên một điện thoại chậm thì độ trễ có thể vẫn quá ngắn.
useDeferredValue không có độ trễ. Lần render nền bắt đầu ngay lập tức, nên trên thiết bị nhanh danh sách cập nhật gần như tức thì, còn trên thiết bị chậm React giữ cho ô nhập phản hồi nhanh và render danh sách thường xuyên nhất có thể. Công việc nền cũng có thể bị ngắt, điều mà một lần render đã debounce không làm được: khi một lần render debounce đã bắt đầu, nó chặn mọi thứ cho đến khi xong.
Điều nó không làm được là giảm số request mạng. Nếu mỗi giá trị mới khởi động một lần fetch, giá trị trì hoãn vẫn thay đổi theo mỗi lần render hoàn tất, nên hãy debounce chính request khi số lượng request là điều quan trọng.
Lỗi thường gặp
- Quên
memo. Như đã thấy ở trên, component chậm phải có khả năng bỏ qua lần render khẩn cấp. Nếu nó không được bọc trongmemo, hoặc một prop khác của nó là object mới ở mỗi lần render, nó sẽ render hai lần cho mỗi lần gõ phím thay vì một. - Truyền một object mới.
useDeferredValue({ query })tạo một object mới ở mỗi lần render, nên giá trị trì hoãn luôn có vẻ đã thay đổi và React lần nào cũng bắt đầu thêm một lần render nền. Hãy trì hoãn giá trị nguyên thủy (useDeferredValue(query)) rồi tạo object từ nó. - Mong đợi ít request hơn. Một component fetch dữ liệu dựa trên giá trị trì hoãn vẫn fetch cho mọi giá trị render xong.
Khi nào nên dùng
Dùng useDeferredValue khi một ô nhập điều khiển một phần trang render chậm, phần chậm đó có thể memo được, và bạn chấp nhận hiển thị kết quả hơi cũ trong chốc lát: kết quả tìm kiếm, một biểu đồ vẽ lại theo bộ lọc, bản xem trước của văn bản đã định dạng. Nếu phần chậm chậm vì một lý do bạn sửa được, như render 5.000 hàng trong khi chỉ thấy được 50, hãy sửa điều đó trước. Với phiên bản của mẫu này khi bạn sở hữu hàm set, xem useTransition.
Câu hỏi thường gặp
useDeferredValue dùng để làm gì?
Nó trả về một phiên bản của giá trị có thể chậm hơn giá trị thật. Khi giá trị thay đổi, React render trước với giá trị trì hoãn cũ, rồi render lại ở chế độ nền với giá trị mới, và lần render nền đó có thể bị ngắt giữa chừng.
Vì sao useDeferredValue không làm danh sách nhanh hơn?
Component chậm phải được bọc trong memo và nhận giá trị trì hoãn. Nếu không, lần render khẩn cấp vẫn render component chậm, chỉ là với giá trị cũ, và việc gõ vẫn chậm.
useDeferredValue và useTransition khác nhau thế nào?
useTransition bọc lệnh cập nhật state, nên bạn cần truy cập được hàm set. useDeferredValue bọc một giá trị, nên nó dùng được khi bạn chỉ nhận giá trị, chẳng hạn qua prop hoặc từ một hook bạn không kiểm soát.
useDeferredValue có phải là debounce không?
Không. Không có độ trễ cố định nào. React bắt đầu lần render trì hoãn ngay sau lần render khẩn cấp, và trên một thiết bị nhanh thì độ trễ gần như không thấy được. Tự nó cũng không giảm số request mạng.
Đối số thứ hai của useDeferredValue là gì?
Từ React 19, bạn có thể truyền một initialValue. Ở lần render đầu, hook trả về giá trị này, sau đó React render lại ở chế độ nền với giá trị thật.