إضافة وظيفة السحب والإفلات في تطبيقات React باستخدام مكتبة React Beautiful DnD
مقدمة إلى السحب والإفلات في React
تُعد وظيفة السحب والإفلات (Drag and Drop) من التقنيات الشائعة التي تُضاف إلى واجهات المستخدم لتمكين الأشخاص من تحريك العناصر على الصفحة بشكل بديهي وتفاعلي. سواء كان ذلك لإعادة ترتيب قائمة مهام، أو تنظيم بطاقات في لوحة مشاريع، فإن هذه الميزة تُسهم بشكل كبير في تحسين تجربة المستخدم وتفاعله مع التطبيق. في عالم React، توفر العديد من المكتبات هذه الإمكانية، ولكن تبرز مكتبة React Beautiful DnD كخيار ممتاز بفضل سهولتها، أدائها العالي، وتركيزها على إمكانية الوصول.
سيرشدك هذا المقال خطوة بخطوة حول كيفية دمج وظيفة السحب والإفلات في تطبيق React الخاص بك، مع التركيز على إنشاء محتوى حصري ومفيد لضمان أفضل تجربة للمستخدم.
ما هي وظيفة السحب والإفلات (Drag and Drop)؟
وظيفة السحب والإفلات هي بالضبط ما يوحي به اسمها: تفاعل يسمح للمستخدم بالنقر على عنصر، سحبه إلى موقع جديد، ثم إفلاته هناك. غالبًا ما يترتب على هذا الإجراء تأثير جانبي داخل التطبيق، مثل تغيير ترتيب العناصر أو نقلها بين مجموعات مختلفة.

يُعد هذا التفاعل شائعًا جدًا في تطبيقات مثل قوائم المهام (To-Do Lists) أو لوحات إدارة المشاريع (Project Management Dashboards)، حيث تحتاج إلى تحديد أولويات المهام وإنشاء ترتيب معين لكيفية إنجازها. على الرغم من أن السحب والإفلات يمكن أن يكون له حالات استخدام متقدمة، إلا أننا سنركز في هذا الدليل على وظائف القائمة الأساسية.
ما هي مكتبة React Beautiful DnD؟
React Beautiful DnD هي مكتبة سحب وإفلات قوية ومُحسّنة لإمكانية الوصول، تم تطويرها بواسطة فريق Atlassian. إذا لم تكن على دراية بـ Atlassian، فهم الفريق الذي يقف وراء Jira، وهي إحدى أكبر أدوات Agile لإدارة المشاريع على الإنترنت حاليًا. كان هدف الفريق هو توفير إمكانيات السحب والإفلات مع مراعاة إمكانية الوصول بشكل خاص، بالإضافة إلى الحفاظ على أداء عالٍ وواجهة برمجة تطبيقات (API) قوية وسهلة الاستخدام.
ماذا سنبني في هذا الدليل؟
سنبدأ بقائمة بسيطة ونضيف إليها القدرة على السحب والإفلات لإعادة ترتيب عناصرها. في هذا الدليل، لن نقضي وقتًا في بناء القائمة نفسها من الصفر. القائمة التي سنستخدمها تعتمد على قائمة HTML غير مرتبة قياسية (<ul>) وعناصر قائمة (<li>) مع بعض التنسيقات البسيطة باستخدام CSS لجعلها تبدو كبطاقات. سنركز بشكل أساسي على إضافة وظيفة السحب والإفلات لإعادة ترتيب القائمة باستخدام مكتبة React Beautiful DnD.
الخطوة 0: إعداد تطبيق React.js جديد
للبدء، نحتاج إلى تطبيق React بسيط يحتوي على قائمة من العناصر. يمكن أن يكون هذا مشروعًا موجودًا لديك أو مشروعًا جديدًا تمامًا تم إنشاؤه باستخدام أداتك المفضلة مثل Create React App. في هذا المثال، سنبدأ بتطبيق جديد تم إنشاؤه باستخدام Create React App، وسنضيف قائمة بسيطة من شخصيات مسلسل Final Space.

إذا كنت ترغب في البدء من نفس النقطة، يمكنك استنساخ المستودع التجريبي الخاص بي من الفرع المحدد. سيقوم هذا الأمر باستنساخ الفرع لبدء العمل:
git clone --single-branch --branch part-0-starting-point git@github.com:colbyfayock/my-final-space-characters.git
بدلاً من ذلك، يمكنك استنساخ المستودع بشكل طبيعي والتبديل إلى الفرع part-0-starting-point. إذا كنت تفضل متابعة الكود فقط، فقد أنشأت أولاً مصفوفة من الكائنات (objects):
const finalSpaceCharacters = [
{ id: 'gary', name: 'Gary Goodspeed', thumb: '/images/gary.png' },
// ... المزيد من الشخصيات
];
ثم أقوم بالتكرار عبر هذه المصفوفة لإنشاء قائمتي:
<ul className="characters">
{finalSpaceCharacters.map( ( {id, name, thumb} ) => {
return (
<li key={id}>
<div className="characters-thumb">
<img src={thumb} alt={`${ name } Thumb`}/>
</div>
<p>{ name }</p>
</li>
);
})}
</ul>
الخطوة 1: تثبيت مكتبة React Beautiful DnD
الخطوة الأولى هي تثبيت المكتبة عبر مدير الحزم npm أو yarn. داخل مشروعك، قم بتشغيل أحد الأوامر التالية:
yarn add react-beautiful-dnd
# أو
npm install react-beautiful-dnd --save
سيؤدي هذا إلى إضافة المكتبة إلى مشروعنا، وسنكون جاهزين لاستخدامها في تطبيقنا.
الخطوة 2: جعل القائمة قابلة للسحب والإفلات باستخدام React Beautiful DnD
بعد تثبيت المكتبة، يمكننا الآن منح قائمتنا القدرة على السحب والإفلات.
إضافة سياق السحب والإفلات إلى تطبيقنا
في أعلى الملف، قم باستيراد المكون DragDropContext من المكتبة:
import { DragDropContext } from 'react-beautiful-dnd';
سيوفر DragDropContext لتطبيقنا القدرة على استخدام المكتبة. يعمل هذا المكون بشكل مشابه لـ Context API في React، حيث يمكن للمكتبة الآن الوصول إلى شجرة المكونات. ملاحظة هامة: إذا كنت تخطط لإضافة وظيفة السحب والإفلات إلى أكثر من قائمة واحدة، فيجب عليك التأكد من أن DragDropContext يغلف جميع هذه العناصر، ربما في جذر تطبيقك. لا يمكنك تضمين DragDropContext داخل DragDropContext آخر.
سنقوم بتغليف قائمتنا باستخدام DragDropContext:
<DragDropContext>
<ul className="characters">
{/* ... عناصر القائمة هنا ... */}
</ul>
</DragDropContext>
في هذه المرحلة، لن يتغير شيء في التطبيق ويجب أن يتم تحميله كما كان من قبل.
جعل القائمة منطقة إسقاط (Droppable)
بعد ذلك، نريد إنشاء منطقة قابلة للإفلات (Droppable)، مما يعني أننا سنحدد منطقة معينة حيث يمكن تحريك عناصرنا داخلها. أولاً، أضف المكون Droppable إلى عبارة الاستيراد في أعلى الملف:
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
لغرضنا، نريد أن تكون قائمتنا غير المرتبة بأكملها (<ul>) هي منطقة الإفلات، لذلك سنقوم بتغليفها بهذا المكون:
<DragDropContext>
<Droppable droppableId="characters">
{(provided) => (
<ul className="characters">
{/* ... عناصر القائمة هنا ... */}
</ul>
)}
</Droppable>
</DragDropContext>
ستلاحظ أننا قمنا بتغليفها بشكل مختلف هذه المرة. أولاً، قمنا بتعيين droppableId على مكون <Droppable> الخاص بنا. يسمح هذا للمكتبة بتتبع هذه النسخة المحددة بين التفاعلات. نقوم أيضًا بإنشاء دالة مباشرة داخل هذا المكون تمرر الوسيط provided. ملاحظة: يمكن أن تمرر هذه الدالة وسيطين بما في ذلك وسيط snapshot، لكننا لن نستخدمه في هذا المثال.
يتضمن الوسيط provided معلومات ومراجع للكود الذي تحتاجه المكتبة للعمل بشكل صحيح. لاستخدامه، على عنصر قائمتنا، دعنا نضيف:
<ul className="characters" {...provided.droppableProps} ref={provided.innerRef}>
سيؤدي هذا إلى إنشاء مرجع (provided.innerRef) للمكتبة للوصول إلى عنصر HTML الخاص بالقائمة. كما يطبق خصائص (provided.droppableProps) على العنصر تسمح للمكتبة بتتبع الحركات وتحديد المواقع. مرة أخرى، في هذه المرحلة، لن تكون هناك أي وظائف ملحوظة.
جعل العناصر قابلة للسحب (Draggable)
الآن للجزء الممتع! القطعة الأخيرة لجعل عناصر قائمتنا قابلة للسحب والإفلات هي تغليف كل عنصر قائمة بمكون مشابه لما فعلناه للتو مع القائمة بأكملها. سنستخدم مكون Draggable، والذي، مرة أخرى، على غرار مكون Droppable، سيتضمن دالة سنمرر من خلالها الخصائص إلى مكونات عناصر قائمتنا.
أولاً، نحتاج إلى استيراد Draggable جنبًا إلى جنب مع بقية المكونات:
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
بعد ذلك، داخل حلقتنا التكرارية، دعنا نغلف عنصر القائمة العائد بمكون <Draggable /> ودالته العليا:
{finalSpaceCharacters.map( ( {id, name, thumb} ) => {
return (
<Draggable>
{(provided) => (
<li key={id}>
{/* ... محتوى عنصر القائمة ... */}
</li>
)}
</Draggable>
);
})}
نظرًا لأن لدينا الآن مكونًا جديدًا في المستوى الأعلى في حلقتنا التكرارية، دعنا ننقل خاصية key من عنصر القائمة إلى Draggable:
{finalSpaceCharacters.map( ( {id, name, thumb} ) => {
return (
<Draggable key={id}>
{(provided) => (
<li>
{/* ... محتوى عنصر القائمة ... */}
</li>
)}
</Draggable>
);
})}
نحتاج أيضًا إلى تعيين خاصيتين إضافيتين على <Draggable>: وهما draggableId و index. سنحتاج إلى إضافة index كوسيط إلى دالة map الخاصة بنا، ثم تضمين هذه الخصائص على مكوننا:
{finalSpaceCharacters.map( ( {id, name, thumb}, index ) => {
return (
<Draggable key={id} draggableId={id} index={index}>
{(provided) => (
<li>
{/* ... محتوى عنصر القائمة ... */}
</li>
)}
</Draggable>
);
})}
أخيرًا، نحتاج إلى تعيين بعض الخصائص على عنصر القائمة نفسه. على عنصر قائمتنا، أضف هذا المرجع ref وانشر الخصائص الإضافية من الوسيط provided:
<Draggable key={id} draggableId={id} index={index}>
{(provided) => (
<li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
{/* ... محتوى عنصر القائمة ... */}
</li>
)}
</Draggable>
الآن، إذا قمنا بتحديث صفحتنا وحومنا فوق عناصر قائمتنا، يمكننا الآن سحبها وتحريكها!

ومع ذلك، ستلاحظ أنه عندما تبدأ في تحريك عنصر ما، يبدو أن الجزء السفلي من الصفحة به بعض المشاكل. هناك بعض مشكلات تجاوز المحتوى (overflow issues) مع عناصر قائمتنا وتذييل الصفحة. ستلاحظ أيضًا في وحدة تحكم المطور (developer console)، أن React Beautiful DnD يعطينا رسالة تحذير بأننا نفتقد شيئًا يسمى placeholder.

إضافة عنصر نائب (Placeholder) من React Beautiful DnD
جزء من متطلبات React Beautiful DnD هو أننا نضيف عنصرًا نائبًا (placeholder). هذا شيء توفره المكتبة جاهزًا، ويُستخدم لملء المساحة التي كان يشغلها العنصر الذي نسحبه سابقًا. لإضافة ذلك، نريد تضمين provided.placeholder في الجزء السفلي من مكون القائمة العليا Droppable، في حالتنا في الجزء السفلي من <ul>:
<Droppable droppableId="characters">
{(provided) => (
<ul className="characters" {...provided.droppableProps} ref={provided.innerRef}>
{/* ... عناصر القائمة ... */}
{provided.placeholder}
</ul>
)}
</Droppable>
وإذا بدأنا في سحب الأشياء في متصفحنا، يمكننا أن نرى أن تدفق صفحتنا لا يحتوي على مشاكل وأن المحتوى يبقى في مكانه الصحيح!

المشكلة الأخيرة هي أنه عندما تحرك شيئًا ما، فإنه لا يبقى في مكانه الجديد. فكيف يمكننا حفظ ترتيب عناصرنا؟
الخطوة 3: حفظ ترتيب القائمة بعد إعادة ترتيب العناصر
عندما نحرك عناصرنا، فإنها تبقى حيث تهبط لجزء من الثانية. ولكن بعد أن تنتهي React Beautiful DnD من عملها، ستتم إعادة عرض شجرة المكونات لدينا. عندما تتم إعادة عرض المكونات، تعود عناصرنا إلى نفس المكان الذي كانت فيه من قبل، لأننا لم نحفظ ذلك خارج ذاكرة مكتبة DnD.
لحل هذه المشكلة، يأخذ DragDropContext خاصية onDragEnd تسمح لنا بتشغيل دالة بعد اكتمال السحب. تمرر هذه الدالة وسائط تتضمن الترتيب الجديد لعناصرنا حتى نتمكن من تحديث حالة تطبيقنا لدورة العرض التالية.
حفظ عناصر القائمة في حالة التطبيق (State)
أولاً، دعنا نخزن عناصرنا في حالة التطبيق (state) حتى يكون لدينا شيء لتحديثه بين الدورات. في أعلى الملف، أضف useState إلى استيراد React:
import React, { useState } from 'react';
بعد ذلك، سنقوم بإنشاء حالتنا باستخدام قائمتنا الافتراضية من العناصر. أضف ما يلي إلى أعلى مكون App الخاص بنا:
const [characters, updateCharacters] = useState(finalSpaceCharacters);
نظرًا لأننا سنقوم بتحديث حالة characters الجديدة لتوفير عناصر قائمتنا وترتيبها، فسنرغب الآن في استبدال المصفوفة التي نمررها بحالتنا الجديدة:
<ul className="characters" {...provided.droppableProps} ref={provided.innerRef}>
{characters.map( ( {id, name, thumb}, index ) => {
{/* ... محتوى عنصر القائمة ... */}
})}
وإذا قمنا بالحفظ وتحديث صفحتنا، فلا ينبغي أن يتغير شيء!
تحديث حالة التطبيق عند سحب العناصر
الآن بعد أن أصبح لدينا حالتنا، يمكننا تحديث هذه الحالة في أي وقت يتم فيه سحب عناصر قائمتنا. يأخذ مكون DragDropContext الذي أضفناه إلى صفحتنا خاصية onDragEnd. كما يوحي الاسم، ستشغل هذه الخاصية دالة كلما توقف شخص عن سحب عنصر في القائمة. دعنا نضيف دالة handleOnDragEnd كخاصية لنا:
<DragDropContext onDragEnd={handleOnDragEnd}>
بعد ذلك، نحتاج إلى أن تكون هذه الدالة موجودة بالفعل. يمكننا تعريف دالة أسفل تعريف الحالة:
function handleOnDragEnd (result) {
// منطق التعامل مع نهاية السحب
}
تأخذ دالتنا وسيطًا يسمى result. إذا أضفنا console.log(result) إلى الدالة وحركنا عنصرًا في قائمتنا، يمكننا أن نرى أنه يتضمن تفاصيل حول ما يجب أن تكون عليه الحالة المحدثة بعد إجراء الحركة.

على وجه الخصوص، نريد استخدام قيمة index في كل من خاصيتي destination و source، والتي تخبرنا بفهرس العنصر الذي يتم نقله وما هو الفهرس الجديد لهذا العنصر في مصفوفة العناصر. لذلك باستخدام ذلك، دعنا نضيف ما يلي إلى دالتنا:
function handleOnDragEnd (result) {
const items = Array.from(characters);
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
updateCharacters(items);
}
إليك ما نقوم به في الكود أعلاه:
- نقوم بإنشاء نسخة جديدة من مصفوفة
charactersالخاصة بنا. - نستخدم قيمة
source.indexللعثور على عنصرنا من مصفوفتنا الجديدة وإزالته باستخدام الدالةsplice. - يتم تفكيك (destructured) هذه النتيجة، لذلك ينتهي بنا المطاف بكائن جديد
reorderedItemوهو عنصرنا المسحوب. - ثم نستخدم
destination.indexلإضافة هذا العنصر مرة أخرى إلى المصفوفة، ولكن في موقعه الجديد، مرة أخرى باستخدامsplice. - أخيرًا، نقوم بتحديث حالة
charactersالخاصة بنا باستخدام الدالةupdateCharacters.
والآن بعد حفظ دالتنا، يمكننا تحريك شخصياتنا، وهي تحفظ موقعها الجديد!

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

المشكلة هي أنه عندما نسحبه خارج الحاوية المحددة، لا يكون لدينا وجهة (destination). لتجنب ذلك، يمكننا ببساطة إضافة عبارة فوق الكود الذي يحرك عنصرنا تتحقق مما إذا كانت الوجهة موجودة، وإذا لم تكن كذلك، تخرج من الدالة:
function handleOnDragEnd (result) {
if (!result.destination) return;
const items = Array.from(characters);
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
updateCharacters(items);
}
وإذا قمنا بإعادة تحميل الصفحة وحاولنا سحب عنصرنا مرة أخرى، يعود عنصرنا إلى الموقع الأصلي دون خطأ!

ميزات إضافية لمكتبة React Beautiful DnD
ما قمنا بتغطيته هو الأساسيات، ولكن React Beautiful DnD تقدم المزيد من الميزات القوية:
تخصيص الأنماط أثناء السحب والإفلات
عند تحريك العناصر، توفر المكتبة لقطة (snapshot) للحالة المعطاة. باستخدام هذه المعلومات، يمكننا تطبيق أنماط مخصصة بحيث عندما نحرك عناصرنا، يمكننا إظهار حالة نشطة للقائمة، أو للعنصر الذي نحركه، أو لكليهما! هذا يضيف لمسة بصرية رائعة ويحسن من وضوح التفاعل.
يمكنك استكشاف المزيد من الأمثلة على الأنماط المخصصة في توثيق React Beautiful DnD.
السحب بين قوائم متعددة
إذا كنت قد استخدمت Trello من قبل أو أداة مشابهة، فيجب أن تكون على دراية بمفهوم الأعمدة المختلفة التي يمكنك سحب البطاقات بينها لتحديد أولويات مهامك وأفكارك وتنظيمها. تسمح لك هذه المكتبة بفعل الشيء نفسه، مما يوفر القدرة على سحب وإفلات العناصر من منطقة سحب واحدة إلى أخرى.
لمزيد من التفاصيل حول السحب بين قوائم متعددة، راجع أمثلة React Beautiful DnD.
الخلاصة التقنية
تُعد مكتبة React Beautiful DnD حلاً ممتازًا وموثوقًا لتطبيق وظيفة السحب والإفلات في تطبيقات React. إنها لا توفر فقط واجهة برمجة تطبيقات قوية ومرنة، بل تضع أيضًا إمكانية الوصول والأداء في صميم تصميمها، مما يضمن تجربة مستخدم سلسة وشاملة. من خلال بضع خطوات بسيطة، تمكنا من تحويل قائمة ثابتة إلى قائمة تفاعلية بالكامل، مع القدرة على إعادة ترتيب العناصر وحفظ التغييرات في حالة التطبيق. إن الاهتمام بالتفاصيل مثل العناصر النائبة ومعالجة الأخطاء خارج الحدود يعكس نضج المكتبة ويجعلها الخيار الأمثل للمطورين الذين يسعون لتقديم تجربة سحب وإفلات احترافية في تطبيقاتهم.