useDeferredValue: تأخیر در خود مقدار، نه در تابع تغییردهنده
در دو درس قبل، با startTransition و useTransition دیدیم چگونه یک فراخوانی set را کماولویت علامت بزنیم. اما گاهی، خود مقداری که باید کماولویت باشد، از بیرون (مثلاً از طریق Props) به کامپوننت میرسد و ما کنترلی روی فراخوانی set آن نداریم. برای این موقعیت، React ابزار دیگری ارائه میدهد: useDeferredValue.
مشکلی که useTransition بهتنهایی حل نمیکند
function SearchResults({ query }) {
// query از کامپوننت والد میآید؛ اینجا کنترلی روی لحظه setQuery نداریم
const results = filterLargeList(query);
return <ul>{results.map((r) => <li key={r}>{r}</li>)}</ul>;
}
function SearchPage() {
const [query, setQuery] = useState("");
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<SearchResults query={query} />
</div>
);
}
در این ساختار، SearchResults نمیداند و نمیتواند تعیین کند که فراخوانی setQuery در SearchPage، داخل startTransition پیچیده شده یا نه؛ چون این تصمیم، در کامپوننت دیگری گرفته میشود. اگر SearchResults بخواهد خودش، مستقل از والدش، محاسبه سنگین داخلیاش را کماولویت کند، باید راه دیگری داشته باشد.
راهحل: useDeferredValue
import { useDeferredValue } from "react";
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
const results = filterLargeList(deferredQuery);
return <ul>{results.map((r) => <li key={r}>{r}</li>)}</ul>;
}
useDeferredValue، یک مقدار میگیرد و یک نسخه «تأخیری» از همان مقدار برمیگرداند. وقتی query بهسرعت تغییر میکند، deferredQuery کمی عقبتر، با یک تأخیر کنترلشده توسط خود React (نه یک عدد ثابت میلیثانیه)، همان مقدار را دنبال میکند؛ این یعنی محاسبه سنگین filterLargeList، روی مقدار تأخیری اجرا میشود، نه مقدار آنی.
تفاوت کلیدی با useTransition: محل اعمال
| ابزار | جایی که اعمال میشود |
|---|---|
| useTransition | دور خود فراخوانی set، در کامپوننتی که تغییر را آغاز میکند |
| useDeferredValue | دور یک مقدار دریافتی، در کامپوننتی که آن مقدار را مصرف میکند |
این تفاوت، دقیقاً پاسخ همان مشکل ابتدای درس است: وقتی کامپوننت مصرفکننده، کنترلی روی لحظه تغییر مقدار ندارد، useDeferredValue به او اجازه میدهد، مستقل از والد، تصمیم بگیرد با نسخه کمی عقبتر مقدار کار کند.
تشخیص وضعیت «در حال عقب ماندن»
برای نشان دادن یک نشانه بصری (شبیه isPending در useTransition)، میتوان مقدار فعلی و مقدار تأخیری را مقایسه کرد:
const isStale = query !== deferredQuery;
<div style={{ opacity: isStale ? 0.6 : 1 }}>
<ResultsList query={deferredQuery} />
</div>
وقتی query و deferredQuery برابر نباشند، یعنی React هنوز مشغول رساندن نسخه تأخیری به مقدار واقعی است؛ همان لحظهای که میتوان یک افکت بصری ملایم نشان داد.
useDeferredValue، خودش محاسبه را memo نمیکند
یک نکته مهم: useDeferredValue، بهتنهایی باعث نمیشود filterLargeList کمتر فراخوانی شود؛ فقط زمانبندی فراخوانی آن را، با اولویت پایینتر، به تعویق میاندازد. اگر محاسبه، فارغ از این تأخیر، بیشازحد تکراری هم باشد، ترکیب آن با useMemo (فصل سیزدهم) همچنان ارزشمند است؛ این دو ابزار، دو مشکل متفاوت (تکرار غیرضروری در برابر زمانبندی) را حل میکنند.
مثال قابل اجرا
import { useState, useDeferredValue } from "react";
function filterLargeList(query) {
const items = Array.from({ length: 15000 }, (_, i) => "آیتم " + i);
if (!query) return items.slice(0, 15);
return items.filter((item) => item.includes(query)).slice(0, 15);
}
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
const results = filterLargeList(deferredQuery);
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<ul>
{results.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
function App() {
const [query, setQuery] = useState("");
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="جستوجو..." />
<SearchResults query={query} />
</div>
);
}جمعبندی
useDeferredValue، برخلاف useTransition که دور خود فراخوانی set اعمال میشود، دور یک مقدار دریافتی در کامپوننت مصرفکننده اعمال میشود؛ مناسب برای زمانی که کنترلی روی لحظه تغییر آن مقدار از بیرون نداریم. این ابزار، زمانبندی را تغییر میدهد، نه تعداد خود محاسبه را؛ برای آن، همچنان useMemo لازم است. در درس بعدی، تفاوت کلی Urgent و Non-Urgent Updates را جمعبندی میکنیم.
