useDeferredValue: تأخیر در خود مقدار، نه در تابع تغییردهنده

در دو درس قبل، با startTransition و useTransition دیدیم چگونه یک فراخوانی set را کم‌اولویت علامت بزنیم. اما گاهی، خود مقداری که باید کم‌اولویت باشد، از بیرون (مثلاً از طریق Props) به کامپوننت می‌رسد و ما کنترلی روی فراخوانی set آن نداریم. برای این موقعیت، React ابزار دیگری ارائه می‌دهد: useDeferredValue.

مشکلی که useTransition به‌تنهایی حل نمی‌کند

jsx
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

jsx
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)، می‌توان مقدار فعلی و مقدار تأخیری را مقایسه کرد:

jsx
const isStale = query !== deferredQuery;

<div style={{ opacity: isStale ? 0.6 : 1 }}>
  <ResultsList query={deferredQuery} />
</div>

وقتی query و deferredQuery برابر نباشند، یعنی React هنوز مشغول رساندن نسخه تأخیری به مقدار واقعی است؛ همان لحظه‌ای که می‌توان یک افکت بصری ملایم نشان داد.

useDeferredValue، خودش محاسبه را memo نمی‌کند

یک نکته مهم: useDeferredValue، به‌تنهایی باعث نمی‌شود filterLargeList کمتر فراخوانی شود؛ فقط زمان‌بندی فراخوانی آن را، با اولویت پایین‌تر، به تعویق می‌اندازد. اگر محاسبه، فارغ از این تأخیر، بیش‌ازحد تکراری هم باشد، ترکیب آن با useMemo (فصل سیزدهم) همچنان ارزشمند است؛ این دو ابزار، دو مشکل متفاوت (تکرار غیرضروری در برابر زمان‌بندی) را حل می‌کنند.

مثال قابل اجرا

تأخیر در مقدار دریافتی با useDeferredValue
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>
  );
}
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

useDeferredValue، برخلاف useTransition که دور خود فراخوانی set اعمال می‌شود، دور یک مقدار دریافتی در کامپوننت مصرف‌کننده اعمال می‌شود؛ مناسب برای زمانی که کنترلی روی لحظه تغییر آن مقدار از بیرون نداریم. این ابزار، زمان‌بندی را تغییر می‌دهد، نه تعداد خود محاسبه را؛ برای آن، همچنان useMemo لازم است. در درس بعدی، تفاوت کلی Urgent و Non-Urgent Updates را جمع‌بندی می‌کنیم.