AbortController خارج حدود fetch: أنماط الإلغاء التي نستخدمها في كل تطبيق React
جدول المحتويات
عن الكاتب

Ahmed Mahmoud
كاتب ومطوّر
مهندس برمجيات شغوف بتطوير الويب وتصميم تجربة المستخدم.
مؤسّس ديفيا · eng-ahmed.com ↗أهم النقاط
`AbortController` ينشئ `AbortSignal`، واستدعاء `controller.abort(reason)` يحوّل `signal.aborted` إلى true ويرفض أي طلب `fetch` جارٍ مرتبط بهذه الإشارة.
كائن AbortController يعمل لمرة واحدة؛ بعد الإلغاء يبقى ملغى، لذلك يحتاج كل طلب جديد إلى كائن جديد.
`AbortSignal.timeout(ms)` يعيد إشارة تُلغى تلقائيًا بخطأ `TimeoutError` من نوع DOMException، وتغني عن التوليفة اليدوية `setTimeout` مع `abort()` مع `clearTimeout`.
`AbortSignal.any([a, b])` يدمج مصادر الإلغاء: الإشارة الناتجة تُلغى بمجرد إلغاء أول إشارة من المدخلات.
إلغاء الطلب يوقف الاستجابة ولا يلغي العمل المنفذ على الخادم؛ لا نعتمد على الإلغاء للتراجع عن عمليات الكتابة، ونلغي القراءات بحرية.
ما هو AbortController وكيف تعمل الإشارة فعليًا؟
AbortController صنف مدمج في المتصفح وNode.js ينتج إشارة AbortSignal واحدة، وهذه الإشارة هي الطريقة القياسية في منصة الويب لقول «أوقف هذا العمل». ننشئ الكائن، نمرر خاصيته `signal` إلى أي واجهة برمجية تقبلها، ثم نستدعي `controller.abort(reason)` عندما لا نعود بحاجة إلى العمل. كل مستهلك للإشارة يتفاعل فورًا: طلب `fetch` الجاري يُرفض، والمستمعون المسجلون بالإشارة يُزالون، ويمكن لكودنا فحص `signal.aborted` أو استدعاء `signal.throwIfAborted()` داخل الحلقات.
النقطة التي تربك الفرق: الكائن يعمل لمرة واحدة ولا توجد إعادة تعيين. بعد استدعاء `abort()` تبقى الإشارة ملغاة للأبد، وأي fetch يبدأ بها يفشل فورًا. كل طلب جديد يحصل على كائن AbortController جديد.
كيف نلغي طلب fetch قديمًا داخل useEffect في React؟
السباق الكلاسيكي: حقل بحث يرسل طلبًا مع كل ضغطة مفتاح، فتصل استجابة بطيئة قديمة بعد استجابة أحدث وتستبدل النتائج الجديدة ببيانات قديمة. الحل هو إلغاء الطلب السابق في دالة تنظيف التأثير — ننشئ `AbortController` داخل `useEffect`، نمرر `controller.signal` إلى `fetch`، ونعيد `() => controller.abort()` كدالة تنظيف — بحيث لا يصل إلى الحالة سوى أحدث طلب.
React ينفذ التنظيف قبل إعادة تشغيل التأثير، لذلك تؤدي الكتابة السريعة إلى إلغاء كل طلب قديم لحظة بدء التالي. نفس التنظيف يعمل عند إزالة المكون أيضًا، ما يقضي على تحذيرات setState بعد إزالة المكون من المصدر بدل حراسة الكود بعلم `isMounted`. ومن يستخدم TanStack Query يحصل على هذا تلقائيًا — دالة `queryFn` تستقبل `{ signal }` وكل المطلوب تمريرها إلى `fetch`.
ماذا يستبدل AbortSignal.timeout()؟
`AbortSignal.timeout(ms)` يعيد إشارة تُلغى تلقائيًا بعد المدة المحددة بالمللي ثانية، فتصبح المهلة تعبيرًا واحدًا: `fetch(url, { signal: AbortSignal.timeout(8000) })`. وهو يستبدل النمط الذي اعتادت أغلب قواعد الكود كتابته يدويًا: إنشاء كائن، ثم `setTimeout` لاستدعاء `abort()`، ثم تذكر `clearTimeout` عند النجاح.
تفصيلان مهمان: أولًا، إشارة المهلة تُلغى بخطأ `TimeoutError` بينما الإلغاء اليدوي ينتج `AbortError`، فيستطيع معالج الأخطاء التمييز بين انتهاء المهلة والإلغاء المتعمد. ثانيًا، `AbortSignal.timeout()` متاح في كل المتصفحات الحديثة وفي Node.js 18 فما فوق، لذا يعمل نفس الكود في معالجات المسارات والمهام على الخادم أيضًا.
كيف ندمج مهلة زمنية مع زر إلغاء للمستخدم؟
`AbortSignal.any(signals)` يستقبل مصفوفة إشارات ويعيد إشارة واحدة تُلغى بمجرد إلغاء أي منها — وهو المعادل في عالم الإلغاء لدالة `Promise.race()`. أكثر استخداماتنا شيوعًا: طلب توليد ذكاء اصطناعي طويل يجب أن يتوقف عند ضغط المستخدم على «إلغاء» أو عند انقضاء مهلة صارمة، أيهما أولًا: `AbortSignal.any([controller.signal, AbortSignal.timeout(30000)])`.
خاصية `reason` في الإشارة المدمجة تأخذ قيمة المصدر الذي أُلغي أولًا، لذا يبقى `err.name` قادرًا على إخبارنا هل ألغى المستخدم (`AbortError`) أم انتهت المهلة (`TimeoutError`). الدالة أحدث من بقية الواجهة — Chrome 116 وSafari 17.4 وFirefox 124 وNode.js 20.3 — لكنها مغطاة في كل المتصفحات دائمة التحديث ونستخدمها دون polyfill.
هل تستطيع إشارة واحدة إزالة عدة مستمعي أحداث دفعة واحدة؟
نعم. `addEventListener` يقبل خيار `signal`، وإلغاء الإشارة يزيل كل مستمع سُجل بها. هذا هو نمطنا الافتراضي في تفاعلات السحب واختصارات لوحة المفاتيح وأي عنصر واجهة يربط مستمعين بـ `window` أو `document`: لا مراجع دوال مخزنة، ولا استدعاءات `removeEventListener` مطابقة، واستدعاء `abort()` واحد يفكك كل شيء.
وفي React يتكامل هذا مع `useEffect`: نسجل كل المستمعين بإشارة واحدة ونعيد `() => controller.abort()` كدالة تنظيف، فلا يمكن للتنظيف أن ينحرف عن الإعداد.
كيف نفرق بين الإلغاء والفشل الحقيقي؟
طلب fetch الملغى يُرفض، لذلك تتدفق حالات الإلغاء إلى نفس كتلة `catch` التي تصل إليها أخطاء الشبكة الحقيقية — ولو أبلغنا عنها دون تمييز امتلأ متتبع الأخطاء بالضجيج كلما غادر مستخدم الصفحة أثناء طلب جارٍ. المميِّز هو خاصية `name` في الخطأ: `AbortError` يعني أننا ألغينا عمدًا فنتجاهله، و`TimeoutError` يعني انقضاء المهلة وهو جدير بالعد والقياس، وأي شيء آخر فشل حقيقي يجب إعادة رميه.
وإن مررنا قيمة مخصصة إلى `abort(reason)` تصبح هذه القيمة هي سبب الرفض بدل DOMException الافتراضي — مفيد لحمل سياق إضافي، لكنه يعني أيضًا أن فحص `AbortError` لن يتطابق، لذا نختار اصطلاحًا واحدًا لكل قاعدة كود ونلتزم به.
متى لا ينبغي إلغاء الطلب؟
الإلغاء يوقف الاستجابة ولا يوقف العمل على الخادم. فبحلول لحظة استدعاء `abort()` على طلب POST قد يكون الخادم أتم عملية الكتابة بالفعل — إغلاق الاتصال لا يتراجع عن شيء. قاعدتنا: نلغي القراءات بحرية وبقوة، ونترك الكتابات تكتمل. وإن أراد المستخدم فعلًا إلغاء عملية كتابة، فالتصميم الصادق هو مفتاح idempotency مع طلب تعويضي صريح، لا طلب POST ملغى ربما اكتمل وربما لا.
وللقاعدة مرآة على الخادم: في معالج مسار Next.js تُلغى `request.signal` عند انقطاع اتصال العميل، وينبغي للعمل الطويل المتدفق فحصها كي لا ندفع ثمن مخرجات لا يقرؤها أحد.
الأسئلة الشائعة
س: هل يلغي abort الطلب على الخادم؟
ج: لا. `abort()` يغلق اتصال العميل، لكن أي عمل بدأه الخادم قد يكتمل. تعامل مع الإلغاء كتنظيف على جانب العميل، وصمم عمليات الكتابة بمفاتيح idempotency إن كان الإلغاء مهمًا.
س: هل يمكن إعادة استخدام AbortController بعد استدعاء abort()؟
ج: لا. الكائن يعمل لمرة واحدة — بعد الإلغاء تبقى إشارته ملغاة وأي fetch مرتبط بها يُرفض فورًا. أنشئ كائنًا جديدًا لكل طلب.
س: ما الخطأ الذي يرميه fetch الملغى؟
ج: كائن DOMException باسم `AbortError` عند الإلغاء اليدوي، أو `TimeoutError` عندما تأتي الإشارة من `AbortSignal.timeout()`. وإن مررت سببًا مخصصًا إلى `abort(reason)` يُرفض الطلب بهذه القيمة.
س: هل يستخدم TanStack Query كائن AbortController داخليًا؟
ج: نعم. دالة `queryFn` تستقبل كائنًا يحوي `signal`؛ مرره إلى `fetch` وسيلغي TanStack Query تلقائيًا طلبات الاستعلامات المستبدلة أو المزالة.
س: هل استخدام AbortSignal.any() آمن دون polyfill؟
ج: للمتصفحات دائمة التحديث في 2026 نعم: صدر في Chrome 116 وSafari 17.4 وFirefox 124 وNode.js 20.3. إن كنت تدعم متصفحات أقدم فافحص توافر الدالة وارجع إلى كائن واحد كبديل.
قراءات إضافية
هندسة الواجهات
الوضع الداكن دون وميض في Next.js 16: كيف نستخدم سكربت سمة حاجبًا وcolor-scheme وزر تبديل آمنًا مع الـ Hydration
وميض الوضع الداكن عند إعادة تحميل الصفحة مشكلة توقيت: الخادم لا يستطيع قراءة localStorage. نشرح سكربت السمة الحاجب الذي نستخدمه في Next.js 16، ولماذا لا نضع السمة في cookie حفاظًا على الرسم الثابت، وكيف نبني زر تبديل يمر بالـ hydration دون أي عدم تطابق.
هندسة الواجهات
صور Open Graph الديناميكية في Next.js 16: كيف نبني opengraph-image.tsx ونتحقق من إصدار Satori الذي تضمّنه next/og ونحافظ على صحة معاينات الروابط
واجهت بطاقات المشاركة المولّدة لدينا ثلاث مفاجآت: خط WOFF2 لم يستطع المحرّك قراءته، وملف README لـ Satori يصف محرّكًا أحدث من المضمّن في Next.js، وصفحة ظلّت تعرض صورة الـ layout. هذه ملاحظاتنا الميدانية حول opengraph-image.tsx في Next.js 16 وأولوية البيانات الوصفية وفحص og:image قبل مشاركة أي رابط.