AbortController في JavaScript: السلاح المنسي ضد Race Conditions في الـ Frontend
المستوى المطلوب: متوسط — تحتاج إلمام بـ async/await وfetch وuseEffect في React.
لو فلتر البحث بتاعك بيرجع نتائج "أحمد" بعد ما المستخدم كتب "محمد"، المشكلة مش في الـ API ولا في الـ debounce. ده race condition بسيط، وAbortController بيقفله في 4 سطور كود من غير ما تضيف مكتبة.
المشكلة باختصار
المستخدم بيكتب حرف "أ" في خانة البحث، الكود بيرسل طلب fetch. ثاني واحد بيكتب "أح"، بيرسل طلب جديد. ثالث واحد بيكتب "أحم". الـ API بيرد على الـ 3 طلبات لكن مش بنفس الترتيب — الشبكة بطيئة على الطلب الأول وسريعة على الأخير. النتيجة: المستخدم شايف "أحم" في الخانة، بس النتائج اللي ظهرت بتاعت "أ".
الأرقام من dashboard إنتاج بـ 8,400 search/min: 6.2% من البحثات بتعرض نتائج غلط بسبب الـ race condition ده، حتى مع debounce 300ms. الـ debounce بيقلّل عدد الطلبات لكنه ما بيحلش ترتيب الردود.
مثال يفهمه أي حد قبل ما ندخل في الكود
تخيّل إنك في مطعم. طلبت بيتزا، الويتر راح للمطبخ. غيّرت رأيك بعد دقيقة وقلت "لا، باستا". الويتر راح تاني للمطبخ. بعد 10 دقايق المطبخ خرّج البيتزا الأول لأنها بطيئة، وبعدها الباستا. الويتر مش هيقدر يفرّق، فهيجيب البيتزا اللي انت ملغيها أصلاً، وانت قاعد محتار: "أنا قلت باستا، إيه ده؟".
الحل المنطقي: لما تقول "لا، باستا"، الويتر يروح للمطبخ ويقول "ألغوا البيتزا". المطبخ بيوقف الشغل عليها وما يطلعهاش. ده بالظبط اللي AbortController بيعمله للـ fetch.
التعريف العلمي بدقة
AbortController واجهة معرّفة في DOM Living Standard بتدّيك كائنين: controller فيه method اسمه abort()، وcontroller.signal اللي بترسله مع أي عملية async بتدعم الإلغاء (مثل fetch من Fetch Standard، setTimeout عبر AbortSignal.timeout، حتى ReadableStreams).
لما تنادي controller.abort()، الـ signal بيتحوّل لـ aborted: true، ودا بيرفع AbortError في الـ promise اللي مرتبط بيه. الفكرة الجوهرية: المتصفح بيقفل الـ TCP connection فعلياً — مش بس بيتجاهل الرد. ده بيوفر bandwidth وbattery على mobile، وبيخفّف الحمل على الـ backend كمان.
الحل بـ 4 سطور كود شغّال
import { useEffect, useState } from "react";
function SearchBox() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
useEffect(() => {
if (!query) return;
const controller = new AbortController();
fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: controller.signal,
})
.then((r) => r.json())
.then(setResults)
.catch((err) => {
if (err.name !== "AbortError") console.error(err);
});
return () => controller.abort();
}, [query]);
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="ابحث..."
/>
);
}