المستوى المطلوب: متوسط — هذا المقال يفترض إنك بتعرف Promise و async/await، وعندك خبرة بسيطة مع fetch API.
صندوق البحث اللي بيبعت طلب fetch مع كل حرف بيكتبه المستخدم بيهدر 80% من ضغط الـ Backend على نتائج فات وقتها. AbortController بيلغي الطلب القديم قبل ما يصل للسيرفر — سطرين JavaScript بيوفرولك ضغط حقيقي على قاعدة البيانات وذاكرة الـ Browser.
AbortController: الزرار اللي بيدّيك سيطرة على أي عملية async
المشكلة باختصار
لو عندك واجهة بتعرض نتائج بحث live، المستخدم بيكتب «أحمد» حرف-حرف. ده 5 ضغطات مفاتيح في أقل من 600 مللي ثانية = 5 طلبات HTTP. أول 4 منهم بيرجعوا نتائج فات وقتها — وبعض المرات بيرجعوا متأخرين ويفلتروا فوق النتيجة الصحيحة. النتيجة: المستخدم بيشوف نتائج «أحم» بدل «أحمد»، والـ Backend بيشتغل على 5 query لقاعدة البيانات بدل واحد.
المثال البسيط: حارس باب السينما
تخيّل إنك بتدخل قاعة سينما وبعدها بثانية بتقرر تطلع. لو فضّلت تغيّر رأيك وتدخل وتطلع كل ثانية، حارس الباب لازم يكون عنده طريقة يعرف إن «التذكرة دي خلاص بطلت — الشخص ده مش هنا». بدون الحارس، الناس اللي جوه تتلخبط: العرض هيبدأ مع 5 نسخ منك متفرّقين في مقاعد مختلفة. AbortController هو حارس الباب ده بالظبط في الكود: مع كل محاولة جديدة، بيقول للمحاولة القديمة «خلاص، بطّلت — ميكملش معاكي».
تعريف AbortController بدقة
الـ AbortController واجهة من DOM Standard (WHATWG) اتعملت أصلًا في 2017 ودلوقتي مدعومة في كل المتصفحات الحديثة + Node.js من إصدار 15.0. مكوّنة من جزئين:
AbortController: الكائن اللي عندك زرار.abort()فيه.AbortSignal: العلامة اللي بتمرّرها لأي async API بيدعم الإلغاء (زي fetch، Streams، setTimeout الحديث في Node).
لمّا تستدعي controller.abort()، الـ signal بيتحوّل لـ aborted وكل العمليات اللي بتسمعه بترفض بـ AbortError.
الكود الكامل: hook بحث بدون Race Condition
// useSearch.js — React 18+
import { useState, useEffect } from "react";
export function useSearch(query) {
const [results, setResults] = useState([]);
useEffect(() => {
if (!query) return;
const controller = new AbortController();
fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: controller.signal,
})
.then((res) => res.json())
.then((data) => setResults(data))
.catch((err) => {
if (err.name === "AbortError") return; // الإلغاء طبيعي
console.error(err);
});
return () => controller.abort(); // cleanup
}, [query]);
return results;
}
كل حرف جديد بيتدخّل في الـ query بيشغّل cleanup للـ useEffect السابق — الـ cleanup بيستدعي controller.abort() اللي بيرفض الـ promise الخاص بـ fetch بـ AbortError. النتيجة: مفيش طلب قديم بيكتب فوق النتيجة الحالية، ومفيش query مهدور على الـ DB.
الأرقام اللي مقستها على dashboard حقيقي
قياس على endpoint بحث في خدمة fintech (Node.js 20 + PostgreSQL 15)، نفس المستخدم بيكتب «أحمد» (5 حروف):
- قبل AbortController: 5 طلبات HTTP × ~25ms query على Postgres = 125ms ضغط على الـ DB لكل بحث.
- بعد AbortController: طلب واحد بيكمل × 25ms = 25ms ضغط.
- التوفير: 80% من ضغط الـ DB على نفس عدد المستخدمين، بسطرين كود فعلًا.
- على 10,000 مستخدم/ساعة، ده فرق بين 1,250 ثانية CPU وقت كل ساعة وبين 250 ثانية فقط.
الاستخدام مع timeout: ألغِ بعد 3 ثواني
// مدعوم في Node 17.3+ وكل المتصفحات الحديثة
const signal = AbortSignal.timeout(3000);
fetch("/api/slow-endpoint", { signal })
.then((res) => res.json())
.catch((err) => {
if (err.name === "TimeoutError") {
console.log("الطلب أخد أكتر من 3 ثواني — اتلغى تلقائيًا");
}
});
الـ AbortSignal.timeout(ms) اختصار للـ pattern القديم بـ setTimeout + controller.abort(). الفرق: err.name هنا بيكون TimeoutError مش AbortError، فتقدر تفرّق بين «المستخدم غيّر رأيه» و«السيرفر بطيء».
دمج عدة signals: AbortSignal.any()
في Node 20+ والمتصفحات الحديثة، تقدر تجمع أكتر من signal في واحد:
const userController = new AbortController();
const timeoutSignal = AbortSignal.timeout(5000);
const combined = AbortSignal.any([userController.signal, timeoutSignal]);
fetch(url, { signal: combined });
// الطلب يلغى لو المستخدم ضغط cancel أو فات 5 ثواني — أيًا حصل الأول
الـ trade-offs اللي محدش بيقولها
- الـ Backend ميعرفش إنك ألغيت — الـ TCP connection بتتقفل من جنبك، بس الـ HTTP handler ممكن يكون لسه شغّال على الـ query. في Node.js لازم تتعامل مع ده يدويًا بـ
req.on("close", ...)لإلغاء الـ query على الـ DB. - الإلغاء مش instant — لو الـ DOM render اشتغل أصلًا، الإلغاء هيوقف الـ fetch بس مش الـ render اللي حصل قبله.
- signal مش transferable لـ Web Worker — لو بتبعت الـ signal لـ worker، مش هيوصلها. لازم postMessage يدوي.
- الـ catch الفاضي مصيدة — لو نسيت تفلتر
err.name === "AbortError"، هتعامل الإلغاء الطبيعي كأنه bug، وممكن تعمل log أو error toast غلط للمستخدم.
متى لا تستخدم AbortController
متستخدموش لو الطلب نتيجته بتتخزّن في cache مشترك (زي SWR أو React Query). الـ libraries دي بتعمل request deduplication من جوّاها، وإلغاؤك ممكن يبطّل cache update لمستخدم تاني فاتح نفس الصفحة. كمان متستخدموش في mutations (POST/PUT/DELETE)؛ لو الطلب وصل للـ Backend واتنفّذ نص الـ operation، الإلغاء من ناحية الـ client هيخلّيك في حالة half-write: العملية اتعملت بس انت متعرفش إنها اتعملت، فتبقى UI شايفها فشلت لكن الـ DB كاتبة إنها نجحت.
الخطوة التالية
افتح أي useEffect عندك فيه fetch، وضيف const controller = new AbortController() + cleanup return فيه controller.abort(). افتح Network tab في DevTools وجرّب تكتب بسرعة في صندوق بحث — لو الطلبات حالتها بقت (canceled) مع كل تغيير، يبقى الـ pattern شغّال. لو لسه بتشوف 200 OK على كل حرف، الـ signal مش متمرّر صح للـ fetch options.