المشكلة: تحديث واحد يجمّد الواجهة
بعض تحديثات الحالة رخيصة (فتح/إغلاق قائمة)، وبعضها مكلف (فلترة عشرة آلاف عنصر، تبديل تبويب فيه شجرة مكوّنات ضخمة). في React التقليدي، أي تحديث حالة يُعامَل بنفس الأولوية — فتحديث مكلف يمنع المتصفح من الاستجابة لضغطة زر تالية حتى ينتهي الرندر.
React يوفّر أداتين لتمييز "هذا التحديث غير عاجل، أعطِ الأولوية للتفاعل": useTransition وuseDeferredValue.
useTransition — لتحديثات تتحكّم بها أنت
import { useState, useTransition } from "react";
function TabContainer() {
const [tab, setTab] = useState("about");
const [isPending, startTransition] = useTransition();
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab); // تحديث "غير عاجل"
});
}
return (
<>
<nav>
<button onClick={() => selectTab("about")}>من نحن</button>
<button onClick={() => selectTab("posts")}>المقالات</button>
</nav>
{isPending && <span className="spinner" />}
{tab === "about" ? <AboutPanel /> : <HeavyPostsPanel />}
</>
);
}
isPending:trueأثناء تنفيذ التحديث في الخلفية.startTransition: تُغلِّف تحديث الحالة، لتُعامَل كتحديث قابل للمقاطعة وغير عاجل.
الفائدة العملية: إن ضغط المستخدم تبويبًا آخر أثناء رندر التبويب الثقيل، React يقاطع العمل الجاري ويبدأ فورًا بالتبويب الجديد — بدل أن ينتظر انتهاء الأول.
⚠️ التحديثات داخل
startTransitionيجب أن تكون متزامنة — لا تضعsetTimeoutأوawaitقبل استدعاءsetState، وإلا فلن تُحتسَب Transition:// ❌ لن تُعامَل كـ Transition startTransition(() => { setTimeout(() => setTab(next), 100); });
قيد إضافي مهم: لا تستخدم useTransition للتحكّم بقيمة حقل إدخال (<input value={...}>) — التحديث غير العاجل يجعل الكتابة تبدو متأخّرة. لهذه الحالة تحديدًا يأتي الخطّاف الثاني.
useDeferredValue — لقيمة لا تتحكّم بها
import { useState, useDeferredValue } from "react";
function SearchPage() {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
{/* الحقل يتحدّث فورًا، بينما SearchResults تتأخّر خطوة عند الحاجة */}
<SearchResults query={deferredQuery} />
</>
);
}
useDeferredValue يأخذ قيمة (query) ويُرجع نسخة "متأخّرة" منها. الحقل نفسه يتحدّث فورًا لأنه غير مرتبط بالقيمة المؤجَّلة، بينما SearchResults — إن كانت باهظة الرندر — تستمر لحظيًا بعرض نتائج البحث السابقة إلى أن ينتهي React من تجهيز النتائج الجديدة في الخلفية.
const isStale = query !== deferredQuery;
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<SearchResults query={deferredQuery} />
</div>
مقارنة query !== deferredQuery تخبرك إن كانت النتيجة المعروضة "قديمة" حاليًا، فتقدر تُظهر تعتيمًا بصريًا بسيطًا كإشارة بصرية بدل مؤشر تحميل كامل.
متى أيّهما؟
| المعيار | useTransition | useDeferredValue |
|---|---|---|
| تتحكّم بـ | استدعاء setState نفسه | قيمة (prop/state) وصلتك جاهزة |
| مناسب لـ | تبديل تبويبات، فلاتر بأزرارك الخاصة | قيمة قادمة من مكوّن آخر لا تتحكّم بتحديثها |
| مؤشر "قيد التنفيذ" | isPending جاهز | تقارن القيمتين يدويًا (value !== deferredValue) |
| قيود الإدخال | لا تستخدمه لحقول نصية متحكَّم بها | هو الحل البديل لحقول البحث بالذات |
الأخطاء الشائعة
- ❌ استخدام
useTransitionللتحكّم بقيمة<input>→ الكتابة تبدو بطيئة؛ استخدمuseDeferredValueبدلًا منه. - ❌ تحديث حالة بعد
awaitداخلstartTransitionبلا تغليف إضافي → لن تُعتبَر Transition؛ لف التحديث اللاحق بـstartTransitionمرّة أخرى. - ❌ توقّع أن
useDeferredValueيقلّل عدد طلبات الشبكة → هو لا يمنع الطلبات بحد ذاته، فقط يؤجّل الرندر المكلف.
خلاصة
useTransition يمنحك تحكّمًا مباشرًا بتحديث حالة تعرف أنه غير عاجل (مع isPending جاهز)، بينما useDeferredValue يؤجّل استخدام قيمة وصلتك من الخارج دون أن تتحكّم بمصدرها. كلاهما أدوات "الرندر التزامني" (Concurrent Rendering) في React — تُبقي الواجهة متجاوبة دون تعقيد إضافي في منطق التحميل.