كيفية إنشاء مكوّن React قابل لإعادة الاستخدام فعلاً من الصفر
في هذا الدليل العملي، سنبني تطبيقاً باستخدام React يتيح للمستخدم البحث عن دولة ضمن قائمة، مع عرض اقتراحات مطابقة أسفل حقل الإدخال أثناء الكتابة. لكن الهدف الأهم ليس مجرد تنفيذ ميزة البحث، بل تعلّم كيفية تصميم مكوّن قابل لإعادة الاستخدام فعلاً بحيث يمكن توظيفه في أكثر من موضع داخل المشروع بسهولة وكفاءة.
من خلال هذا الشرح ستتعرّف على كيفية بناء مكوّن اقتراح تلقائي Auto Suggestion من الصفر، واستخدام Hooks مثل useRef وuseEffect، بالإضافة إلى إنشاء Custom Hook خاص لإدارة النقر خارج صندوق الاقتراحات، مع تحسين الأداء بواسطة debounce.
- إنشاء مكوّن
Reactقابل لإعادة الاستخدام. - بناء منطق اقتراحات ذكي ومنفصل عن الواجهة.
- استخدام
useRefلإدارة السلوك بين عمليات إعادة التصيير. - إنشاء
Custom Hookعملي لإخفاء الاقتراحات عند النقر خارج العنصر. - تقليل استدعاءات البحث غير الضرورية لتحسين الأداء.

تهيئة مشروع React لبناء مكوّن الاقتراح التلقائي
سنبدأ بإنشاء المشروع عبر أداة create-react-app، ثم ننظم الملفات الأساسية بطريقة تسهّل فصل المكوّنات والمنطق المخصص.
إنشاء المشروع
npx create-react-app react-autosuggestion-app
بعد إنشاء المشروع، احذف الملفات الافتراضية داخل مجلد src، ثم أنشئ الملفات التالية:
index.jsApp.jsstyles.css
وأنشئ أيضاً المجلدين التاليين داخل src:
componentscustom-hooks
تثبيت الاعتماديات المطلوبة
سنحتاج إلى مكتبات تساعدنا في تنفيذ طلبات الشبكة، وتحسين الأداء، وبناء الواجهة.
yarn add axios@0.21.1 lodash@4.17.21 react-bootstrap@1.6.1 bootstrap@5.1.0
بعد ذلك، أضف التنسيقات المناسبة داخل الملف src/styles.css بحسب تصميمك أو ملف الأنماط الخاص بالمشروع.
بناء المكوّنات الأولية وصفحة البيانات
لجعل التطبيق بسيطاً وسريعاً، سنحفظ قائمة الدول داخل ملف JSON محلي، ثم نقرأها أثناء البحث.
إضافة ملف البيانات
أنشئ ملفاً باسم countries.json داخل مجلد public وضع بداخله قائمة الدول.
إنشاء مكوّن عرض الاقتراحات
أنشئ ملف AutoComplete.js داخل مجلد components وأضف الكود التالي:
import React from 'react';
function AutoComplete({ isVisible, suggestions, handleSuggestionClick }) {
return (
<div className={`${isVisible ? 'show suggestion-box' : 'suggestion-box'}`}>
<ul>
{suggestions.map((country, index) => (
<li key={index} onClick={() => handleSuggestionClick(country)}>
{country}
</li>
))}
</ul>
</div>
);
}
export default AutoComplete;
وظيفة هذا المكوّن بسيطة وواضحة: استقبال قائمة الاقتراحات وعرضها للمستخدم، ثم تمرير العنصر المختار إلى دالة خارجية عند النقر عليه. بهذه الطريقة يبقى المكوّن مسؤولاً عن العرض فقط، بينما يبقى منطق البحث والإدارة في مكان آخر.
إنشاء Custom Hook لإغلاق الاقتراحات عند النقر خارجها
لتحسين تجربة المستخدم، من المهم إخفاء صندوق الاقتراحات عندما ينقر المستخدم في أي مكان خارج القائمة. بدلاً من تكرار هذا المنطق داخل كل مكوّن، سننقله إلى Custom Hook مستقل.
ملف useOutsideClick.js
import { useState, useRef, useEffect } from 'react';
const useOutsideClick = () => {
const [isVisible, setIsVisible] = useState(false);
const ref = useRef();
const handleOutsideClick = () => {
if (ref.current) {
setIsVisible(false);
}
};
useEffect(() => {
document.addEventListener('click', handleOutsideClick);
return () => {
document.removeEventListener('click', handleOutsideClick);
};
}, []);
return [ref, isVisible, setIsVisible];
};
export default useOutsideClick;
كيف يعمل هذا الـ Hook؟
في البداية نعرّف حالة باسم isVisible وقيمتها الابتدائية false لإخفاء صندوق الاقتراحات:
const [isVisible, setIsVisible] = useState(false);
ثم ننشئ مرجعاً باستخدام useRef():
const ref = useRef();
وفي نهاية Hook نُرجع ثلاث قيم:
return [ref, isVisible, setIsVisible];
هذا يعني أن أي مكوّن يستخدم useOutsideClick يمكنه ربط المرجع ref بصندوق الاقتراحات الخاص به، والحصول في الوقت نفسه على حالة الظهور والدالة المسؤولة عن تغييرها.
من خلال الدالة handleOutsideClick نتحقق من وجود ref.current قبل تنفيذ الإخفاء:
const handleOutsideClick = () => {
if (ref.current) {
setIsVisible(false);
}
};
كما نستخدم useEffect لإضافة مستمع حدث click عند تحميل المكوّن، ثم إزالته تلقائياً عند إلغاء تحميله، وهي ممارسة مهمة لتجنّب التسربات البرمجية Memory Leaks.
useEffect(() => {
document.addEventListener('click', handleOutsideClick);
return () => {
document.removeEventListener('click', handleOutsideClick);
};
}, []);
إنشاء مكوّن React قابل لإعادة الاستخدام للبحث والاقتراح
الآن نصل إلى الجزء الأهم: بناء مكوّن واحد يمكن استخدامه أكثر من مرة لعرض حقل إدخال مزوّد باقتراحات ديناميكية.
ملف InputControl.js
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import _ from 'lodash';
import { Form } from 'react-bootstrap';
import AutoComplete from './AutoComplete';
import useOutsideClick from '../custom-hooks/useOutsideClick';
const InputControl = ({ name, label, placeholder }) => {
const [documentRef, isVisible, setIsVisible] = useOutsideClick();
const [suggestions, setSuggestions] = useState([]);
const [selectedCountry, setSelectedCountry] = useState('');
const [searchTerm, setSearchTerm] = useState('');
const [errorMsg, setErrorMsg] = useState('');
const ref = useRef();
useEffect(() => {
ref.current = _.debounce(processRequest, 300);
}, []);
function processRequest(searchValue) {
axios
.get('/countries.json')
.then((response) => {
const countries = response.data;
const result = countries.filter((country) =>
country.toLowerCase().includes(searchValue.toLowerCase())
);
setSuggestions(result);
if (result.length > 0) {
setIsVisible(true);
} else {
setIsVisible(false);
}
setErrorMsg('');
})
.catch(() => setErrorMsg('Something went wrong. Try again later'));
}
function handleSearch(event) {
event.preventDefault();
const { value } = event.target;
setSearchTerm(value);
ref.current(value);
}
function handleSuggestionClick(countryValue) {
setSelectedCountry(countryValue);
setIsVisible(false);
}
return (
<Form.Group controlId="searchTerm">
<Form.Label>{label}</Form.Label>
<Form.Control
className="input-control"
type="text"
value={searchTerm}
name={name}
onChange={handleSearch}
autoComplete="off"
placeholder={placeholder}
/>
<div ref={documentRef}>
{isVisible && (
<AutoComplete
isVisible={isVisible}
suggestions={suggestions}
handleSuggestionClick={handleSuggestionClick}
/>
)}
</div>
{selectedCountry && (
<div className="selected-country">
Your selected country: {selectedCountry}
</div>
)}
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
</Form.Group>
);
};
export default InputControl;
لماذا يُعد هذا المكوّن قابلاً لإعادة الاستخدام؟
هذا المكوّن يجمع بين عدة عناصر مترابطة:
- حقل إدخال.
- منطق البحث.
- عرض الاقتراحات.
- إدارة الاختيار.
- إخفاء الاقتراحات عند الحاجة.
وبما أن هذه الأجزاء مغلّفة داخل مكوّن واحد يستقبل خصائص مثل name وlabel وplaceholder، فيمكن إعادة استخدامه بسهولة في أكثر من نموذج أو شاشة.
دور useOutsideClick داخل InputControl
const [documentRef, isVisible, setIsVisible] = useOutsideClick();
نخزن القيم المعادة من useOutsideClick داخل متغيرات محلية. ويُستخدم documentRef لربط صندوق الاقتراحات بالمرجع الذي يراقب النقرات.
تحسين الأداء باستخدام debounce
من أبرز الأخطاء الشائعة في حقول البحث استدعاء مصدر البيانات مع كل حرف يكتبه المستخدم، ما يؤدي إلى ضغط زائد على التطبيق أو الخادم. لحل هذه المشكلة نستخدم الدالة _.debounce() من مكتبة lodash:
ref.current = _.debounce(processRequest, 300);
هذا السطر يعني أن دالة processRequest لن تُنفذ فوراً، بل بعد مرور 300ms من توقف المستخدم عن الكتابة. وبهذا نقلل عدد الطلبات غير الضرورية ونحسن سرعة الاستجابة.
استخدام useRef هنا مهم جداً، لأن القيمة المخزنة داخل ref.current تستمر بين عمليات إعادة التصيير، على عكس المتغيرات العادية التي يعاد إنشاؤها في كل مرة.
تنفيذ البحث داخل processRequest
function processRequest(searchValue) {
axios
.get('/countries.json')
.then((response) => {
const countries = response.data;
const result = countries.filter((country) =>
country.toLowerCase().includes(searchValue.toLowerCase())
);
setSuggestions(result);
if (result.length > 0) {
setIsVisible(true);
} else {
setIsVisible(false);
}
setErrorMsg('');
})
.catch(() => setErrorMsg('Something went wrong. Try again later'));
}
بعد جلب البيانات من /countries.json، نستخدم الدالة filter() لتصفية الدول التي تحتوي على النص المدخل، مع تجاهل حالة الأحرف عبر toLowerCase(). بعد ذلك:
- نخزن النتائج في
suggestions. - نظهر صندوق الاقتراحات إذا كانت هناك نتائج.
- نخفيه إذا لم نجد أي تطابق.
- نعرض رسالة خطأ مناسبة عند فشل الطلب.
التعامل مع إدخال المستخدم
function handleSearch(event) {
event.preventDefault();
const { value } = event.target;
setSearchTerm(value);
ref.current(value);
}
عند كل تغيير في حقل الإدخال، نحدّث الحالة searchTerm ثم نستدعي الدالة المؤجلة داخل ref.current، والتي ستنفّذ البحث بعد المهلة المحددة.
اختيار عنصر من الاقتراحات
function handleSuggestionClick(countryValue) {
setSelectedCountry(countryValue);
setIsVisible(false);
}
بمجرد أن يضغط المستخدم على اقتراح معيّن، نحدّث القيمة المختارة ونخفي قائمة الاقتراحات. هذا السلوك يقدّم تجربة استخدام واضحة ومنظمة.
ربط المكوّن داخل التطبيق الرئيسي
بعد الانتهاء من بناء المكوّن القابل لإعادة الاستخدام، يمكننا استدعاؤه مباشرة داخل الملف App.js.
import React from 'react';
import { Form } from 'react-bootstrap';
import InputControl from './components/InputControl';
const App = () => {
return (
<div className="main">
<h1>React AutoSuggestion Demo</h1>
<div className="search-form">
<Form>
<InputControl
name="country"
label="Enter Country"
placeholder="Type a country name"
/>
</Form>
</div>
</div>
);
};
export default App;
ولتشغيل التطبيق، استخدم الأمر التالي:
yarn start

عند اختيار أي عنصر من القائمة، ستظهر القيمة المحددة أسفل الحقل، وهو ما يؤكد أن دورة الإدخال والاقتراح والاختيار تعمل بشكل صحيح.
إعادة استخدام المكوّن أكثر من مرة داخل الصفحة نفسها
الميزة الحقيقية لهذا التصميم تظهر عندما تحتاج إلى أكثر من حقل بحث مزوّد باقتراحات. بدلاً من تكرار الكود، يمكنك استدعاء المكوّن نفسه مرات متعددة.
import React from 'react';
import { Form } from 'react-bootstrap';
import InputControl from './components/InputControl';
const App = () => {
return (
<div className="main">
<h1>React AutoSuggestion Demo</h1>
<div className="search-form">
<Form>
<InputControl
name="country"
label="Enter Country"
placeholder="Type a country name"
/>
<InputControl
name="country"
label="Enter Country"
placeholder="Type a country name"
/>
</Form>
</div>
</div>
);
};
export default App;

كل نسخة من InputControl تحتفظ بحالتها الخاصة، بما في ذلك:
- قيمة البحث الحالية.
- نتائج الاقتراحات.
- حالة إظهار القائمة أو إخفائها.
- القيمة التي تم اختيارها.
وهذا ما يجعل المكوّن قابلاً لإعادة الاستخدام فعلاً، وليس مجرد مكوّن مشترك شكلياً.
أفضل ممارسات لتحويل المكوّن إلى عنصر احترافي في المشاريع الكبيرة
فصل البيانات عن المكوّن
في المثال الحالي، يتم تحميل البيانات من ملف countries.json. في المشاريع الحقيقية، يُفضّل أن تجعل مصدر البيانات قابلاً للتمرير عبر props أو عبر دالة خارجية، حتى يصبح المكوّن صالحاً للاستخدام مع الدول أو المدن أو المنتجات أو أي نوع آخر من البيانات.
إتاحة تخصيص سلوك البحث
يمكنك مستقبلاً تمرير دالة مثل getSuggestions إلى المكوّن، بحيث لا يكون مرتبطاً بمصدر واحد للبيانات. هذا الأسلوب يرفع مرونة التصميم ويجعل المكوّن أكثر قابلية للتوسع.
تحسين تجربة المستخدم
- إضافة رسالة عند عدم وجود نتائج.
- دعم التنقل عبر لوحة المفاتيح.
- تمييز الجزء المطابق من النص داخل النتائج.
- عرض مؤشر تحميل أثناء تنفيذ الطلب.
تقليل الطلبات المتكررة
إذا كانت البيانات ثابتة نسبياً، يمكنك تحميلها مرة واحدة ثم تخزينها محلياً بدلاً من تنفيذ axios.get() مع كل عملية بحث. هذا التحسين مهم عند التعامل مع واجهات API حقيقية.
الخلاصة التقنية
إن بناء مكوّن قابل لإعادة الاستخدام في React لا يعتمد فقط على نقل واجهة الإدخال إلى ملف منفصل، بل على تصميم منطق داخلي مرن ومستقل وقابل للتكرار دون تعارض. في هذا المثال، لعب كل من useRef وdebounce وCustom Hook دوراً أساسياً في جعل المكوّن عملياً وقابلاً للتوسع. تقنياً، هذا الأسلوب يُعد مناسباً جداً لبناء عناصر واجهة متكررة تحافظ على الأداء وسهولة الصيانة في آن واحد.