كيفية بناء فلتر بحث باستخدام React وReact Hooks
مقدمة: لماذا تحتاج إلى فلتر بحث داخل تطبيقات React؟
عند إرسال طلب GET إلى واجهة برمجية API، يعود الخادم عادةً بكمية من البيانات الجاهزة للعرض أو المعالجة. لكن التحدّي الحقيقي لا يكمن في جلب البيانات فقط، بل في كيفية إدارتها وتقديم تجربة استخدام سلسة للمستخدم، خاصة عندما يرغب في الوصول السريع إلى عنصر محدد داخل قائمة طويلة.
في هذا المقال، سنتعلّم خطوة بخطوة كيفية إنشاء فلتر بحث في React بالاعتماد على المكونات الوظيفية Functional Components وواجهات React Hooks. الفكرة الأساسية هي جلب قائمة مستخدمين من مصدر خارجي، ثم السماح للمستخدم بالبحث داخل هذه القائمة بطريقة مباشرة وسريعة.

جلب البيانات من API باستخدام طلب GET
الخطوة الأولى تتمثل في استرجاع البيانات من خادم خارجي. يمكنك استخدام أي خدمة توفر بيانات تجريبية، لكن في هذا الشرح سنعتمد على JSONPlaceholder للحصول على قائمة مستخدمين جاهزة.
في هذا المثال، سنعرض البيانات داخل بطاقات تحتوي على اسم المستخدم وبريده الإلكتروني، ثم نضيف في الأعلى حقل بحث يُستخدم لتصفية النتائج.
import React, { useState, useEffect } from 'react' ;
import axios from 'axios' ;
import { Card, Input } from 'semantic-ui-react'
export default function Posts ( ) {
const [APIData, setAPIData] = useState([])
useEffect( () => {
axios.get( `https://jsonplaceholder.typicode.com/users` )
.then( ( response ) => {
setAPIData(response.data);
})
}, [])
return (
<div style={{ padding: 20 }}>
<Input icon='search' placeholder='Search...' />
<Card.Group itemsPerRow={3} style={{ marginTop: 20 }}>
{APIData.map((item) => {
return (
<Card>
<Card.Content>
<Card.Header>{item.name}</Card.Header>
<Card.Description>{item.email}</Card.Description>
</Card.Content>
</Card>
)
})}
</Card.Group>
</div>
)
}

يعرض هذا الكود قائمة المستخدمين كما هي بعد جلبها، لكنه لا يوفّر حتى الآن أي قدرة على التصفية أو البحث.
ما الذي يحدث في هذا الجزء؟
- استخدام
useEffect()لتنفيذ طلبGETمرة واحدة عند تحميل المكون. - استخدام
axios.get()لجلب البيانات من الخادم. - تخزين النتائج داخل الحالة
APIDataعبرsetAPIData. - عرض البيانات باستخدام
map()داخل بطاقات من مكتبةsemantic-ui-react.
التقاط قيمة البحث من حقل الإدخال
لكي نبدأ عملية التصفية، نحتاج أولاً إلى تخزين النص الذي يكتبه المستخدم داخل حقل البحث. ويتم ذلك عبر إنشاء حالة جديدة مخصصة للاستعلام.
const [searchInput, setSearchInput] = useState('');
المتغير searchInput سيحمل النص الحالي الذي يكتبه المستخدم، بينما تتولى الدالة setSearchInput تحديث هذه القيمة.
بعد ذلك، ننشئ دالة تتولى معالجة عملية البحث:
const searchItems = () => {
}
ثم نربط هذه الدالة بالحدث onChange داخل عنصر الإدخال:
<Input icon='search' placeholder='Search...' onChange={() => searchItems()} />
لكن هذه الصيغة لا تمرّر قيمة الإدخال. لذلك يجب تعديلها لالتقاط النص المكتوب فعلياً من خلال e.target.value:
<Input icon='search' placeholder='Search...' onChange={(e) => searchItems(e.target.value)} />
والآن نعدّل الدالة بحيث تستقبل القيمة وتحدّث الحالة:
const searchItems = (searchValue) => {
setSearchInput(searchValue)
}
بهذه الخطوة أصبح بإمكاننا قراءة ما يكتبه المستخدم في الزمن الحقيقي، وهي قاعدة أساسية لأي واجهة بحث تفاعلية.

تصفية البيانات باستخدام filter()
بعد الحصول على قيمة البحث، تأتي المرحلة الأهم: مقارنة هذه القيمة مع البيانات القادمة من API واستخراج العناصر المطابقة فقط.
يمكن تنفيذ ذلك باستخدام الدالة filter() على المصفوفة المخزنة في APIData:
const searchItems = (searchValue) => {
setSearchInput(searchValue)
APIData.filter((item) => {
return Object.values(item).join('').toLowerCase().includes(searchInput.toLowerCase())
})
}
شرح منطق التصفية
Object.values(item)تستخرج جميع القيم الموجودة داخل العنصر.join('')تجمع هذه القيم في سلسلة نصية واحدة.toLowerCase()تحوّل النص إلى أحرف صغيرة لتوحيد المقارنة.includes()تتحقق مما إذا كانت السلسلة تحتوي على نص البحث.
ميزة هذه الطريقة أنها لا تقتصر على حقل واحد فقط، بل يمكنها البحث داخل عدة خصائص للكائن الواحد، ما يجعلها مرنة في المشاريع البسيطة والمتوسطة.
لكن من الأفضل تخزين النتائج المفلترة في متغير واضح:
const filteredData = APIData.filter((item) => {
return Object.values(item).join('').toLowerCase().includes(searchInput.toLowerCase())
})

تخزين نتائج البحث في حالة مستقلة
حتى نتمكن من عرض النتائج المفلترة داخل الواجهة، نحتاج إلى حالة إضافية لتخزين هذه النتائج:
const [filteredResults, setFilteredResults] = useState([]);
ثم نستخدم هذه الحالة داخل دالة البحث:
const searchItems = (searchValue) => {
setSearchInput(searchValue)
const filteredData = APIData.filter((item) => {
return Object.values(item).join('').toLowerCase().includes(searchInput.toLowerCase())
})
setFilteredResults(filteredData)
}
هذه الخطوة مهمة لأنها تفصل بين البيانات الأصلية والبيانات الناتجة عن البحث، مما يسهل إدارة العرض لاحقاً ويمنع ضياع النسخة الأصلية من القائمة.
عرض النتائج المفلترة في واجهة المستخدم
الآن ننتقل إلى الجزء العملي الذي يهم المستخدم مباشرة: إظهار النتائج حسب النص المكتوب. المنطق هنا بسيط:
- إذا كان حقل البحث فارغاً، نعرض جميع البيانات.
- إذا احتوى الحقل على نص، نعرض النتائج المفلترة فقط.
لذلك يمكن تحديث دالة البحث كما يلي:
const searchItems = (searchValue) => {
setSearchInput(searchValue)
if (searchInput !== '') {
const filteredData = APIData.filter((item) => {
return Object.values(item).join('').toLowerCase().includes(searchInput.toLowerCase())
})
setFilteredResults(filteredData)
} else {
setFilteredResults(APIData)
}
}
ثم نستخدم شرطاً في جزء return لتحديد ما الذي سيُعرض:
<Card.Group itemsPerRow={3} style={{ marginTop: 20 }}>
{searchInput.length > 1 ? (
filteredResults.map((item) => {
return (
<Card>
<Card.Content>
<Card.Header>{item.name}</Card.Header>
<Card.Description>{item.email}</Card.Description>
</Card.Content>
</Card>
)
})
) : (
APIData.map((item) => {
return (
<Card>
<Card.Content>
<Card.Header>{item.name}</Card.Header>
<Card.Description>{item.email}</Card.Description>
</Card.Content>
</Card>
)
})
)}
</Card.Group>

وعند حذف النص من الحقل، تعود جميع البيانات للظهور مرة أخرى:

الكود الكامل لبناء فلتر البحث في React
فيما يلي النسخة الكاملة من التطبيق كما ورد في المثال:
import React, { useState, useEffect } from 'react' ;
import axios from 'axios' ;
import { Card, Input } from 'semantic-ui-react'
export default function Posts ( ) {
const [APIData, setAPIData] = useState([])
const [filteredResults, setFilteredResults] = useState([]);
const [searchInput, setSearchInput] = useState('');
useEffect(() => {
axios.get(`https://jsonplaceholder.typicode.com/users`)
.then((response) => {
setAPIData(response.data);
})
}, [])
const searchItems = (searchValue) => {
setSearchInput(searchValue)
if (searchInput !== '') {
const filteredData = APIData.filter((item) => {
return Object.values(item).join('').toLowerCase().includes(searchInput.toLowerCase())
})
setFilteredResults(filteredData)
} else {
setFilteredResults(APIData)
}
}
return (
<div style={{ padding: 20 }}>
<Input icon='search' placeholder='Search...' onChange={(e) => searchItems(e.target.value)} />
<Card.Group itemsPerRow={3} style={{ marginTop: 20 }}>
{searchInput.length > 1 ? (
filteredResults.map((item) => {
return (
<Card>
<Card.Content>
<Card.Header>{item.name}</Card.Header>
<Card.Description>{item.email}</Card.Description>
</Card.Content>
</Card>
)
})
) : (
APIData.map((item) => {
return (
<Card>
<Card.Content>
<Card.Header>{item.name}</Card.Header>
<Card.Description>{item.email}</Card.Description>
</Card.Content>
</Card>
)
})
)}
</Card.Group>
</div>
)
}
ملاحظات تقنية مهمة لتحسين الكود
رغم أن المثال السابق يوضح الفكرة بوضوح، توجد ملاحظة تقنية تستحق الانتباه: داخل الدالة searchItems تم استخدام searchInput مباشرة بعد استدعاء setSearchInput. في بعض الحالات قد لا تُحدّث الحالة فوراً بسبب طبيعة التحديث غير المتزامن في React.
لذلك من الأفضل الاعتماد على searchValue نفسه أثناء التصفية بدلاً من searchInput. هذه الصيغة أكثر دقة واستقراراً:
const searchItems = (searchValue) => {
setSearchInput(searchValue)
if (searchValue !== '') {
const filteredData = APIData.filter((item) => {
return Object.values(item)
.join('')
.toLowerCase()
.includes(searchValue.toLowerCase())
})
setFilteredResults(filteredData)
} else {
setFilteredResults(APIData)
}
}
هذه المعالجة تجعل البحث أكثر اتساقاً، خصوصاً عند الكتابة السريعة أو عند التعامل مع بيانات أكبر حجماً.
أفضل ممارسات إضافية
- استخدم
debounceإذا كانت البيانات كبيرة أو إذا كنت ترسل الطلبات إلى الخادم مباشرة. - احرص على وجود
keyفريد لكل عنصر عند استخدامmap(). - يمكنك تخصيص البحث ليشمل حقولاً محددة مثل
nameوemailبدلاً من البحث في كامل الكائن. - إذا كانت البيانات حساسة أو ضخمة جداً، فقد يكون البحث من جهة الخادم
Server-side Searchخياراً أفضل.
متى يكون البحث من الواجهة الأمامية مناسباً؟
البحث من جهة الواجهة الأمامية Front-end Filtering مناسب عندما تكون البيانات:
- محدودة الحجم.
- محمّلة مسبقاً داخل التطبيق.
- لا تحتاج إلى استعلامات معقدة.
- تستهدف تجربة استخدام فورية دون انتظار استجابة جديدة من الخادم.
أما إذا كانت قاعدة البيانات كبيرة أو كانت نتائج البحث تحتاج إلى ترقيم صفحات Pagination أو فرز متقدم، فمن الأفضل نقل منطق البحث إلى الخادم وتمرير قيمة الاستعلام ضمن عنوان API Endpoint.
الخلاصة التقنية
إنشاء فلتر بحث باستخدام React وReact Hooks يُعد من المهارات الأساسية لأي مطور واجهات أمامية. هذا النوع من الوظائف يحسّن تجربة المستخدم بشكل مباشر، لأنه يسهّل الوصول إلى البيانات بسرعة ويجعل الواجهة أكثر تفاعلاً. تقنياً، يُفضّل هذا الأسلوب عندما تكون البيانات متاحة محلياً وبحجم معقول، مع الانتباه إلى إدارة الحالة بشكل صحيح وتجنّب الاعتماد على قيم قديمة بعد تحديث state. وإذا أردت بناء واجهات أكثر احترافية، فابدأ بهذا النموذج ثم طوّره بإضافة البحث المؤجل debounced search أو الربط مع خادم حقيقي.