AbortController بالعربي: امنع نتائج البحث القديمة من كسر الواجهة
مستوى القارئ: متوسط
هتخرج من المقال ده بطريقة عملية تمنع طلبات fetch القديمة من تحديث الواجهة بنتائج غلط، خصوصًا في search box أو filters بتتغير بسرعة.
المشكلة باختصار
افترض إن عندك صفحة منتجات. المستخدم كتب iph، وبعد 200ms كتب iphone. الواجهة بعتت طلبين: طلب قديم لكلمة iph وطلب جديد لكلمة iphone. اللي بيحصل فعلاً إن الشبكة مش بتضمن ترتيب الرجوع. ممكن الطلب القديم يرجع بعد الجديد، فيعمل setState ويعرض نتائج مش مطابقة لآخر كلمة كتبها المستخدم.
الطريقة الشائعة الغلط هنا إنك تعتمد على إن آخر طلب اتبعت هو آخر طلب هيرجع. الطريقة دي بتفشل مع latency متغير، CDN، mobile network، أو backend عليه ضغط. ركز: المشكلة مش بس في استهلاك الشبكة. المشكلة الأكبر إن UI ممكن تبقى كاذبة.
الفكرة بمثال بسيط
اعتبر كل بحث عامل زي موظف بيطلع يجيب ملف من الأرشيف. لو المستخدم غيّر رأيه وطلب ملفًا ثانيًا، مش منطقي تسيب الموظف الأول يرجع بعد خمس دقائق ويحط الملف القديم على مكتبك. الأفضل تبعت له إشارة واضحة: وقف المهمة دي، الطلب اتغيّر.
في JavaScript، الإشارة دي اسمها AbortSignal، واللي بيصدرها هو AbortController. بتنشئ controller لكل طلب، تمرر controller.signal إلى fetch، ولما الطلب يبقى قديم تستدعي controller.abort(). حسب توثيق MDN، abort() يقدر يوقف طلب fetch وقراءة جسم الاستجابة والـ streams المرتبطة به.
تطبيق فعلي في React
الافتراض إن عندك React component بيجيب نتائج بحث من endpoint اسمه /api/search?q=.... كل مرة query تتغير، هنلغي الطلب السابق قبل ما نبدأ طلب جديد. ده يطابق فكرة cleanup في useEffect: React يشغّل cleanup للـ effect القديم قبل تشغيل effect الجديد عند تغيّر dependencies.
import { useEffect, useState } from "react";
export function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!query.trim()) {
setResults([]);
return;
}
const controller = new AbortController();
async function load() {
setLoading(true);
setError(null);
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: controller.signal,
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
setResults(data.items);
} catch (err) {
if (err.name === "AbortError") return;
setError(err.message);
} finally {
if (!controller.signal.aborted) setLoading(false);
}
}
load();
return () => controller.abort();
}, [query]);
return null;
}