يتيح لك useTransition تعليم تحديث حالة كغير عاجل. تعرض React ذلك التحديث في الخلفية وتسقطه أو تعيد بدءه إذا حدث شيء أكثر إلحاحًا، مثل نقرة أو ضغطة مفتاح، فتبقى الصفحة سريعة الاستجابة بينما يلحق جزء بطيء من الشاشة.
انقر posts ثم، قبل أن تظهر القائمة، انقر contact. يظهر contact فورًا: رمت React عرض posts الذي لم يكتمل. غيّر الآن startTransition(() => { setTab(next); }) إلى setTab(next); عادية وجرّب الشيء نفسه: تتجمد الصفحة نحو 300 جزء من الثانية بعد النقر على posts، وتنتظر نقرتك على contact حتى تنتهي المنشورات.
الصيغة
const [isPending, startTransition] = useTransition();
startTransition(() => {
setTab(next);
});
- تأخذ
startTransitionدالة وتشغّلها فورًا. وأي تحديثات حالة بداخلها تُعلَّم كانتقالات. - تكون
isPendingقيمتهاtrueمن لحظة بدء الانتقال حتى تنتهي React من عرضه وتُثبت النتيجة.
لا يأخذ useTransition وسائط. الدالة التي تمررها تعمل بشكل متزامن؛ فالانتقال يتعلق بكيفية عرض React للتحديث الناتج، لا بوقت تشغيل دالتك.
ماذا يعني «غير عاجل»
تقسم React التحديثات إلى نوعين. التحديثات العاجلة، مثل الكتابة والنقر والضغط، يجب أن تنعكس فورًا وإلا بدت الصفحة معطلة. وتحديثات الانتقال، مثل التبديل إلى عرض جديد أو إظهار نتائج مرشحة، يمكن أن تستغرق لحظة.
تعرض React الانتقال على شرائح صغيرة وتتحقق بين المكوّنات مما إذا وصل شيء عاجل. إن وصل، توقف React الانتقال مؤقتًا أو تتخلى عنه، وتتعامل مع التحديث العاجل، ثم تبدأ الانتقال من جديد بأحدث حالة. وأثناء عرض الانتقال تبقى الشاشة السابقة ظاهرة؛ فلا تعرض React نتيجة نصف مبنية. لهذا يظل المثال يعرض نص about، مع تلميح Loading بجانب الأزرار، حتى تجهز المنشورات.
تحدث المقاطعة بين المكوّنات. مكوّن واحد يحجب 300 جزء من الثانية لا يمكن قطعه نصفين، ولهذا يتوزع العمل البطيء أعلاه على 300 مكوّن صغير من SlowPost.
عرض تلميح الانتظار
isPending هي المكان المناسب لمؤشر تحميل لا يستبدل المحتوى الحالي: مؤشر دوار بجانب الزر، أو نص باهت، أو تسمية "Saving...". في المثال الأول تعرض Loading... بجانب التبويبات. ويمكنك أيضًا استخدامها لتنسيق المحتوى:
<div style={{ opacity: isPending ? 0.6 : 1 }}>{content}</div>
ترشيح قائمة كبيرة أثناء الكتابة
لا يمكنك وضع قيمة الحقل نفسها في انتقال. يجب أن يستقبل <input> المتحكم به قيمته الجديدة في التحديث العاجل نفسه، وإلا ضاعت محارف. احتفظ بقطعتين من الحالة بدلًا من ذلك: نص الحقل (عاجل) والاستعلام الذي تستخدمه القائمة (انتقال).
اكتب 123 بسرعة. لا يتأخر الحقل أبدًا. ما زالت Console تسجّل سطر rendering results واحدًا لكل ضغطة مفتاح، لأن Results يسجّل بمجرد أن تبدأ React عرضه، لكن قائمتي 1 و12 لا تصلان إلى الشاشة أبدًا: قاطعت كل ضغطة أحدث العرض الجاري، وبدأت React من جديد بأحدث استعلام. Results ملفوف بـ memo عمدًا: من دونه سيعرض التحديث العاجل لكل ضغطة مفتاح القائمة البطيئة أيضًا بالاستعلام القديم، وستخسر الفائدة.
startTransition دون الخطاف
تصدّر react أيضًا startTransition كدالة عادية. تعلّم التحديثات كانتقالات تمامًا كما يفعل الخطاف، لكنك لا تحصل على isPending. استخدمها حيث لا يمكنك استدعاء خطاف، مثلًا في مكتبة بيانات أو موجّه خارج أي مكوّن.
import { startTransition } from 'react';
function navigate(url) {
startTransition(() => {
setCurrentUrl(url);
});
}
الانتقالات غير المتزامنة في React 19
منذ React 19 يمكن للدالة التي تمررها أن تكون async. عندها تبقى isPending قيمتها true طوال مدة تشغيل الدالة، بما في ذلك الانتظار، ما يجعلها طريقة بسيطة لعرض حالة انتظار أثناء طلب. الدالة الوهمية saveName أدناه بديل عن طلب حقيقي.
لاحظ استدعاء startTransition الثاني حول setSaved. تفقد React تتبع الانتقال عبر await، لذا يجب تغليف تحديث الحالة بعده من جديد ليُحسب جزءًا من الانتقال. تسمي React الدالة الممررة بهذه الطريقة إجراءً (action)؛ ويُبنى useActionState وإجراءات النماذج على الآلية نفسها.
الانتقال ليس تأخيرًا
التأخير (debounce) ينتظر مدة ثابتة بعد آخر ضغطة مفتاح قبل أن يفعل أي شيء. أما الانتقال فلا ينتظر. يبدأ العرض فورًا، وإذا وصل تحديث أحدث يسقط العمل القديم. على جهاز سريع تظهر النتيجة دون أي تأخير على الإطلاق؛ وعلى جهاز بطيء تفعل React بقدر ما تستطيع بين ضغطات المفاتيح. وما لا يستطيعه الانتقال هو تقليل عدد طلبات الشبكة: إذا بدأت كل ضغطة مفتاح طلبًا، فأخّر الطلب.
الانتقالات وSuspense
تغيّر الانتقالات أيضًا ما تفعله حدود Suspense. عادة، عندما يُعلَّق مكوّن داخل الحدود (مكوّن كسول يُحمَّل، أو بيانات تُقرأ بـ use)، تستبدل React المحتوى بالبديل. أما إذا كان التحديث الذي سبب التعليق انتقالًا، فتُبقي React المحتوى الموجود على الشاشة بدلًا من ذلك، وتبقى isPending قيمتها true حتى يجهز المحتوى الجديد. بهذه الطريقة تتجنب الموجّهات إظهار مؤشر دوار بحجم الصفحة مع كل تنقل: تغلّف التنقل في انتقال، وتبقى الصفحة القديمة ظاهرة حتى يمكن عرض التالية. والمحتوى الذي لم يُعرض قط ما زال يستخدم البديل، فتتصرف الحدود المكشوفة حديثًا كالمعتاد.
أشياء لا تعمل
- التحديثات داخل مهلة زمنية. لا يعلّم
startTransition(() => setTimeout(() => setTab(next), 100))التحديث، لأنه يعمل بعد أن تُرجعstartTransition. ضعstartTransitionداخل المهلة بدلًا من ذلك. - مكوّن بطيء واحد. لا تستطيع React المقاطعة إلا بين المكوّنات، فالمكوّن الواحد الذي يحجب مدة طويلة ما زال يحجب.
- التحكم في حقل. كما سبق، تبقى حالة الحقل نفسها عاجلة.
وللحالة التي لا تملك فيها دالة ضبط الحالة ولا تستقبل إلا قيمة، مثل prop، يعطيك useDeferredValue السلوك نفسه.
الأسئلة الشائعة
ماذا يفعل useTransition؟
يعطيك startTransition، التي تعلّم تحديثات الحالة بداخلها كغير عاجلة، وisPending، التي تكون true ما دام ذلك التحديث قيد العرض. تستطيع React مقاطعة عرض غير عاجل لتتعامل مع النقرات والكتابة أولًا.
ما الفرق بين useTransition وstartTransition؟
كلاهما يعلّم التحديثات كانتقالات. ويعطيك الخطاف useTransition أيضًا isPending لتلميح تحميل. أما startTransition المستقلة المستوردة من react فتعمل خارج المكوّنات لكن ليس لها علامة انتظار.
هل يمكنني استخدام useTransition لحقل نصي؟
ليس لقيمة الحقل نفسها. يجب أن يتحدث الحقل المتحكم به بشكل متزامن وإلا انكسرت الكتابة. أبقِ حالة الحقل عاجلة وضع التحديث المكلف الذي يعتمد عليها في انتقال، أو استخدم useDeferredValue.
هل useTransition هو نفسه التأخير (debounce)؟
لا. التأخير ينتظر مدة ثابتة قبل أن يفعل أي شيء. أما الانتقال فيبدأ العرض فورًا ويُقاطع إذا وصل تحديث أحدث، فتظهر النتيجة على جهاز سريع دون أي تأخير.
هل يمكن أن تأخذ startTransition دالة غير متزامنة؟
نعم، منذ React 19. تبقى isPending قيمتها true حتى تنتهي الدالة غير المتزامنة. ويجب تغليف تحديثات الحالة بعد await في استدعاء startTransition آخر لتُعلَّم كانتقال.