تخطّى إلى المحتوى الرئيسي
مكوّن Activity في React داخل Next.js 16: كيف نتعامل مع الصفحات المخفية وإعادة تشغيل التأثيرات وuseEffectEvent

مكوّن Activity في React داخل Next.js 16: كيف نتعامل مع الصفحات المخفية وإعادة تشغيل التأثيرات وuseEffectEvent

October 28, 2026
هندسة الواجهات
8 دقيقة قراءة

أهم النقاط

مكوّن Activity في React، المستقر منذ الإصدار React 19.2، يحتفظ بحالة React وبعناصر DOM لجزء من الواجهة، لكنه يخفيه باستخدام display: none ويُشغّل دالة التنظيف لكل تأثير (Effect) بداخله، كما لو أن هذا الجزء أُزيل من الشجرة.

عند تفعيل cacheComponents: true في Next.js 16، يخفي Next.js الصفحات داخل Activity بدلًا من إزالتها، ويحتفظ بما يصل إلى 3 صفحات؛ وتُحذف أقدمها وتُعرض من جديد عند زيارتها التالية.

الآثار الجانبية لعناصر DOM تستمر رغم الإخفاء: عناصر video وaudio وiframe، ووسوم style على مستوى الصفحة، والقوائم المفتوحة، كلها تحتاج إلى إعادة ضبط صريحة داخل دالة تنظيف في useLayoutEffect أو في callback ref.

التأثيرات تُعاد مع كل دورة إخفاء وإظهار، لذلك يُبقي useEffectEvent القيم غير التفاعلية خارج مصفوفة الاعتماديات دون إعادة اتصال إضافية.

المحتوى المخفي داخل Activity يبقى في المستند، لذا تحتاج الاختبارات الشاملة إلى استعلامات تراعي الظهور: الدالة getByRole في Playwright تتجاهل العناصر المخفية، أما محدد CSS المجرد فلا يتجاهلها.

ماذا يفعل مكوّن Activity في React فعلًا؟

Activity مكوّن في React صدر مع React 19.2 في 1 أكتوبر 2025، ويخفي جزءًا من شجرة المكوّنات ثم يستعيده دون إزالته. الخاصية الوحيدة له بجانب children هي mode، وقيمتها إما 'visible' (الافتراضية) أو 'hidden'.

عندما يكون Activity مخفيًا، يضبط display: none على العناصر الأبناء، ويُشغّل دالة التنظيف لكل تأثير بداخله، ويستمر في إعادة عرض الأبناء بأولوية أقل عند تغيّر خصائصهم. وعند العودة إلى 'visible' يظهر نفس DOM بنفس الحالة، وتُشغَّل دوال الإعداد للتأثيرات من جديد.

حالة React والمراجع (refs): تضيع مع العرض الشرطي، وتبقى مع Activity المخفي.

حالة DOM مثل قيم الحقول وموضع التمرير وعناصر details المفتوحة وموضع تشغيل الفيديو: تضيع مع العرض الشرطي، وتبقى مع Activity المخفي.

التأثيرات: تُنظَّف في الحالتين، ويعيد Activity إنشاءها عند الظهور مرة أخرى.

إعادة العرض عند تغيّر الخصائص: لا تحدث بعد الإزالة، بينما يستمر Activity المخفي في إعادة العرض بأولوية أقل.

لماذا تحتفظ صفحة Next.js 16 بحالتها بعد الانتقال إلى صفحة أخرى؟

تحتفظ صفحة Next.js 16 بحالتها بعد الانتقال عند ضبط cacheComponents: true في ملف next.config.ts، لأن Next.js يخفي حينها الصفحة السابقة داخل Activity بدلًا من إزالتها. يحتفظ Next.js بما يصل إلى 3 صفحات بهذه الطريقة، وبعد ذلك تُحذف أقدم صفحة وتُعرض من جديد عند زيارتها التالية. ومن دون Cache Components يظل App Router يزيل الصفحات عند الانتقال.

قبل Cache Components كان فريقنا يحافظ على لوحات الفلاتر والمسودات عبر رفع الحالة إلى layout مشترك أو إلى مخزن خارجي. أما الآن فالاحتفاظ بالحالة هو السلوك الافتراضي، فيصبح كل جزء من حالة الصفحة قرارًا مستقلًا: فلاتر البحث والمسودات يجب أن تبقى، بينما القائمة المنسدلة المفتوحة ورسالة 'تم الإرسال!' والحالة الخاصة بالمستخدم السابق يجب ألا تبقى.

عند تغيّر المستخدم نستخدم معرّف المستخدم كـ key للنماذج المرتبطة به، ونُسجّل الخروج بإعادة تحميل كاملة عبر window.location.href بدلًا من router.push(). وللصفحات التي يجب أن تبدأ من جديد مع كل نقرة على رابط، تضيف وثائق Next.js 16.3 الخاصية useRouter().bfcacheId: عند استخدامها كـ key لعنصر Fragment تُعيد ضبط الشجرة الفرعية مع نقرات Link وrouter.push() وrouter.replace()، مع استعادة الحالة عند الرجوع والتقدّم في المتصفح. وتصفها الوثائق بأنها أداة ترحيل في المقام الأول.

ما الذي يتعطل عندما تُخفى الصفحة بدلًا من إزالتها؟

المكوّن المخفي يتصرف كمكوّن مُزال بالنسبة لتأثيراته، وكمكوّن موجود بالنسبة لعناصر DOM الخاصة به، لذلك يستمر أي شيء تفعله عناصر DOM بنفسها. هذه هي الحالات التي نراجعها في كل صفحة قابلة للحفظ.

الوسائط: عناصر video وaudio وiframe تستمر في التشغيل رغم display: none، بعدما كانت الإزالة توقفها تلقائيًا.

الحالة المفتوحة المؤقتة: القائمة المنسدلة أو النافذة المنبثقة التي كانت مفتوحة عند مغادرة المستخدم تبقى مفتوحة عند عودته.

الأنماط على مستوى الصفحة: وسم style الذي يضبط متغيرات CSS أو z-index على :root يستمر في التأثير على الصفحة الظاهرة.

المنطق الخاص بالتركيب الأول فقط: التأثير الذي يضع التركيز على حقل إدخال عندما تصبح isDialogOpen صحيحة لا يعمل مجددًا إذا حُفظت isDialogOpen بقيمة true. ويحل دليل Next.js ذلك باشتقاق حالة النافذة من معامل في عنوان URL.

المعرّفات المكررة: قيمة id الثابتة في مكوّن يظهر في صفحتين محفوظتين تصبح موجودة مرتين في المستند نفسه، والدالة document.getElementById() وخاصية htmlFor في label تشيران إلى أول تطابق في ترتيب الشجرة، وقد يكون هو النسخة المخفية. والدالة useId() في React تمنع هذا التعارض.

الحل للوسائط والحالة المفتوحة والأنماط واحد: إعادة ضبطها داخل دالة تنظيف، وهي الآن تعمل عند الإخفاء كما تعمل عند الإزالة. وتستخدم وثائق React وNext.js الخطاف useLayoutEffect لهذا الغرض، لأن دالة التنظيف فيه تعمل بشكل متزامن لحظة إخفاء الواجهة، بينما قد تتأخر دالة تنظيف التأثير العادي بسبب حد Suspense يعود إلى التعليق أو بسبب View Transition. ففي مشغّل دروس مثلًا، تستدعي دالة تنظيف واحدة في useLayoutEffect الأمر video.pause() وتغلق قائمة سرعة التشغيل، ويحتفظ الفيديو بموضع currentTime لأن عنصر DOM لا يُحذف أبدًا.

المراجع من نوع callback ref تتبع الدورة نفسها. فمنذ React 19 يمكن لدالة ref أن تُرجع دالة تنظيف، ويُشغّلها Activity عند الإخفاء؛ ويستخدم دليل Next.js ذلك لتعطيل وسم style الخاص بالصفحة بضبط خاصية media على 'not all' ثم إعادة تفعيله بقيمة فارغة.

أين يأتي دور useEffectEvent عندما تُعاد التأثيرات مع كل ظهور؟

useEffectEvent خطاف في React، مستقر منذ React 19.2، يغلّف المنطق الذي يُطلق من داخل تأثير بحيث يقرأ دائمًا أحدث الخصائص والحالة دون أن يصبح من اعتماديات التأثير. وتزداد أهميته مع Activity لأن كل دورة إخفاء وإظهار تُنهي كل اشتراك ثم تعيد إنشاءه، والتأثير الذي يضع تفضيلات الواجهة ضمن اعتمادياته يعيد الاتصال أيضًا كلما تغيّر أحد هذه التفضيلات.

أوضح مثال لدينا هو لوحة طلبات مباشرة: اتصال WebSocket واحد لكل متجر، مع تفضيل للصوت وفلتر للحالة يحددان ما إذا كان الطلب الجديد يُصدر تنبيهًا صوتيًا. يجب أن يعيد storeId وحده إنشاء الاتصال. لذلك نغلّف معالج الرسائل في useEffectEvent ليقرأ أحدث قيم الصوت والفلتر، ولا يضع التأثير الذي يفتح الاتصال ضمن اعتمادياته سوى storeId. وأثناء إخفاء الصفحة تغلق دالة التنظيف الاتصال، فلا تحتفظ الصفحة المحفوظة بأي اتصال مفتوح، وعند ظهورها يعيد التأثير الاتصال مرة واحدة.

القواعد التي تفرضها eslint-plugin-react-hooks بدءًا من الإصدار 6: استدعِ Effect Event فقط داخل useEffect أو useLayoutEffect أو useInsertionEffect أو داخل Effect Event آخر في المكوّن نفسه، وليس أثناء العرض أبدًا؛ ولا تمرّره إلى مكوّنات أبناء أو خطافات أخرى؛ ولا تضعه أبدًا في مصفوفة الاعتماديات. وتتغير هويته مع كل عرض عن قصد، فيظهر الخطأ فورًا إذا اعتمد عليه تأثير.

useEffectEvent ليس وسيلة لإسكات أداة الفحص. إذا كان تغيّر pageUrl يجب أن يسجّل زيارة جديدة، فإن pageUrl يبقى في مصفوفة الاعتماديات. وللمنطق الذي يجب أن يعمل عند التركيب الأول فقط، مثل حدث ظهور يُسجَّل مرة واحدة، نستخدم علمًا عبر useRef(false)، لأن المراجع لا يُعاد ضبطها عندما يخفي Activity الشجرة الفرعية.

هل يستطيع مكوّن Activity تحميل تبويب قبل أن يفتحه المستخدم؟

نعم. الحد الذي يبدأ في الوضع المخفي يعرض أبناءه بأولوية منخفضة دون تشغيل تأثيراتهم، فيمكن تحميل الشيفرة والبيانات وملفات CSS والصور قبل إظهار المحتوى. لكن البيانات التي تُقرأ عبر Suspense فقط، مثل وعد (Promise) يُفك باستخدام use()، هي التي تُجلب أثناء هذا العرض المسبق؛ أما fetch() داخل useEffect فينتظر حتى يصبح الحد ظاهرًا.

النمط الذي نستخدمه هو Server Component يبدأ الاستعلام ويمرر الوعد غير المحلول إلى مكوّن عميل يغلّف المحتوى في Activity وSuspense ويقرأ الوعد باستخدام use(). كما تشارك حدود Activity في الترطيب الانتقائي (Selective Hydration) مثل حدود Suspense، فيستطيع React جعل بقية الصفحة تفاعلية قبل ترطيب شجرة فرعية ثقيلة. ومع ذلك لا نغلّف كل تبويب في Activity: الأشجار المخفية تحتفظ بعناصر DOM في الذاكرة وتُعاد عرضها عند تغيّر خصائصها.

كيف نمنع Playwright من مطابقة المحتوى المخفي داخل Activity؟

نستخدم استعلامات تراعي الظهور، لأن المحتوى المخفي داخل Activity يبقى في المستند مع display: none وتظل محددات CSS العادية تطابقه. الدوال getByRole وgetByLabel وgetByPlaceholder في Playwright تتجاهل العناصر المخفية، بينما قد تُرجع page.locator('.product-card').first() بطاقة من صفحة غادرها المستخدم بالفعل، لذلك نضيف إلى محددات CSS المرشح filter({ visible: true }) صراحةً.

وللكشف عن التأثيرات التي لا تُنظَّف كما يجب، يُعد Strict Mode أرخص فحص: إذ تشير وثائق React إلى أنه يُجري عمليات إزالة وتركيب Activity مبكرًا في بيئة التطوير، فتظهر التأثيرات التي تُعدّ شيئًا ولا تُنهيه أبدًا.

الأسئلة الشائعة

س: هل Activity المخفي هو نفسه إخفاء المكوّن باستخدام CSS؟

ج: لا. إخفاء العنصر بـ display: none وحده يترك التأثيرات تعمل، فتبقى المؤقتات والاشتراكات والاتصالات نشطة. أما حد Activity المخفي فيُشغّل أيضًا دالة التنظيف لكل تأثير ويخفض أولوية عرض الشجرة الفرعية، مع الاحتفاظ بحالتها وعناصر DOM الخاصة بها.

س: هل يحتفظ Next.js 16 بكل صفحة تمت زيارتها؟

ج: لا. الاحتفاظ بالصفحات عبر Activity يعمل فقط عند تفعيل cacheComponents: true، ويحتفظ Next.js بثلاث صفحات كحد أقصى؛ وتُحذف أقدمها وتُعرض من جديد عند زيارتها التالية.

س: هل يجب أن يحل useEffectEvent محل نمط 'آخر قيمة داخل ref'؟

ج: نعم بالنسبة للدوال التي تُطلق من تأثيراتك الخاصة، فـ useEffectEvent يؤدي المهمة نفسها وتفهمه أداة فحص خطافات React. احتفظ بنمط ref فقط عندما يجب تمرير الدالة إلى مكوّن أو خطاف آخر، وهو ما لا تسمح به Effect Events.

س: هل تُعاد عرض المكوّنات داخل Activity المخفي؟

ج: نعم. عندما تتغير خصائص الحد المخفي يعيد React عرض أبنائه بأولوية أقل من المحتوى الظاهر، لكن تأثيراتهم تبقى متوقفة حتى يصبح الحد ظاهرًا مرة أخرى.

س: هل تستطيع قارئات الشاشة الوصول إلى المحتوى المخفي داخل Activity؟

ج: لا. الخاصية display: none تزيل المحتوى المخفي من شجرة إمكانية الوصول. لكن قيم id الثابتة قد تتعارض مع الصفحة الظاهرة، لذا يجب أن تستخدم التسميات ومراجع ARIA معرّفات من useId().