كيفية بناء فلتر بحث باستخدام React وReact Hooks

دقائق القراءة: 6

مقدمة: لماذا تحتاج إلى فلتر بحث داخل تطبيقات React؟

عند إرسال طلب GET إلى واجهة برمجية API، يعود الخادم عادةً بكمية من البيانات الجاهزة للعرض أو المعالجة. لكن التحدّي الحقيقي لا يكمن في جلب البيانات فقط، بل في كيفية إدارتها وتقديم تجربة استخدام سلسة للمستخدم، خاصة عندما يرغب في الوصول السريع إلى عنصر محدد داخل قائمة طويلة.

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

واجهة توضيحية لمشروع React يشرح بناء فلتر بحث ديناميكي باستخدام 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>
  )
}

عرض قائمة المستخدمين مع حقل البحث في الأعلى داخل تطبيق React

يعرض هذا الكود قائمة المستخدمين كما هي بعد جلبها، لكنه لا يوفّر حتى الآن أي قدرة على التصفية أو البحث.

ما الذي يحدث في هذا الجزء؟

  • استخدام 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)
}

بهذه الخطوة أصبح بإمكاننا قراءة ما يكتبه المستخدم في الزمن الحقيقي، وهي قاعدة أساسية لأي واجهة بحث تفاعلية.

التقاط النص المدخل في حقل البحث داخل React وعرضه أثناء الاختبار

تصفية البيانات باستخدام 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

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

إظهار جميع البيانات من جديد عند تفريغ حقل البحث في React

الكود الكامل لبناء فلتر البحث في 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 أو الربط مع خادم حقيقي.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *