المستوى المطلوب: متوسط — هذا المقال يفترض إنك بتعرف 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;
}