كيفية بناء تطبيق اختبار تفاعلي باستخدام React: دليل شامل مع نصائح وكود جاهز

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

في هذا الدليل المخصص للمبتدئين في إطار عمل React، سنتعلم كيفية بناء تطبيق اختبار تفاعلي (Quiz App) من الصفر. سنستكشف معًا كيفية التعامل مع كائنات الحالة (State Objects) المعقدة، إدارة خطافات الحالة (State Hooks) المتنوعة، وعرض المكونات ديناميكيًا بناءً على حالة التطبيق. لنلقِ نظرة على ما سنبنيه:

مثال لتطبيق اختبار تفاعلي مبني باستخدام React

ابدأ رحلتك البرمجية: تحديات تطبيق الاختبار

إذا كنت تفضل خوض التحدي بنفسك قبل الغوص في الشرح التفصيلي، فإليك السيناريوهات الرئيسية التي يجب أن يحققها تطبيقك (يمكنك أيضًا الحصول على الكود الأولي أدناه):

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

الكود الأولي (Starter Code)

يمكنك الحصول على الكود الأولي للمشروع مباشرة من مستودع GitHub عبر هذا الرابط: رابط الكود الأولي على GitHub.

هيا بنا نبدأ!

هيكلة التطبيق: عرض السؤال الأول

عند فتح الكود الأولي والتوجه إلى ملف App.js، ستجد قائمة بالأسئلة والإجابات مخزنة في مصفوفة (array) باسم questions. هذه هي قاعدة بيانات اختبارنا. هدفنا الأول هو استخلاص بيانات السؤال من هذه المصفوفة وعرضها على الشاشة. في البداية، سنقوم بإزالة النص المكتوب يدويًا (hardcoded text) واستخدام بيانات السؤال الأول فقط لتبسيط العملية، وسنهتم بآلية التبديل بين الأسئلة لاحقًا.

في جزء JSX الخاص بنا، استبدل نص السؤال المكتوب يدويًا بالرمز {questions[0]} للوصول إلى العنصر الأول (أو السؤال الأول) في مصفوفة الأسئلة لدينا.

<div className= 'question-text' >{questions[ 0 ]}</div>

هذا الكود يعرض السؤال الأول من المصفوفة. لاحظ أننا نستخدم الفهرس 0 للوصول إلى العنصر الأول.

عرض السؤال وخيارات الإجابة

بما أن السؤال الأول هو كائن (object)، يمكننا استخدام طريقة الوصول بالنقطة (dot notation) للوصول إلى خصائصه. الآن، سنستخدم {questions[0].questionText} للحصول على نص السؤال لهذا الكائن تحديدًا:

<div className= 'question-text' >{questions[ 0 ].questionText}</div>

احفظ التغييرات وشغل التطبيق. ستلاحظ تحديث النص ليظهر نص السؤال الأول ديناميكيًا. تذكر أننا ما زلنا نأخذ نص السؤال الأول فقط من الكائن الأول في مصفوفة questions.

سنتبع نهجًا مشابهًا لخيارات الإجابة. قم بإزالة الأزرار المكتوبة يدويًا وسنستخدم دالة map للتكرار على خيارات الإجابة لسؤال معين. تذكر أن دالة map تقوم بالتكرار على عناصر المصفوفة وتوفر لنا العنصر الحالي الذي يتم التكرار عليه في شكل متغير. استبدل عنصر div الذي يحمل الفئة answer-section بالآتي:

<div className= 'answer-section' >
 {questions[ 0 ].answerOptions.map( ( answerOption, index ) => (
 < button > {answerOption.answerText} </ button >))}
</div>

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

دعنا نلخص ما تعلمناه حتى الآن:

  • نحن نحصل على السؤال الأول من مصفوفة questions باستخدام questions[0].
  • السؤال الأول هو عبارة عن كائن يحتوي على مصفوفة من answerOptions. يمكننا الوصول إلى هذه المصفوفة باستخدام طريقة الوصول بالنقطة: questions[0].answerOptions.
  • نظرًا لأن answerOptions هي مصفوفة، يمكننا التكرار عليها باستخدام دالة map: questions[0].answerOptions.map.
  • داخل دالة map، نقوم بعرض زر لكل answerOption، ونعرض النص الخاص به.

تغيير الأسئلة ديناميكيًا باستخدام الحالة (State)

لنعد الآن إلى جزء JSX الخاص بنا. ستلاحظ أنه إذا قمنا بتغيير questions[0] إلى questions[1]، أو questions[2]، فإن واجهة المستخدم (UI) ستتحدث. يحدث هذا لأنها تستمد البيانات من أسئلة مختلفة في مصفوفة questions، اعتمادًا على الفهرس.

ما نرغب في تحقيقه هو استخدام كائن حالة (state object) للاحتفاظ برقم السؤال الحالي الذي يتواجد عليه المستخدم، وتحديث هذا الكائن عند النقر على زر الإجابة. يمكنك رؤية هذا السلوك عند تشغيل الكود في المثال النهائي.

قم بإضافة كائن حالة جديد، والذي سيحتفظ برقم السؤال الحالي للمستخدم. سيتم تهيئته بالقيمة 0 لكي يبدأ الاختبار بالسؤال الأول من المصفوفة:

 const [currentQuestion, setCurrentQuestion] = useState( 0 );

الآن، نريد استبدال القيمة '0' المكتوبة يدويًا في JSX بهذا المتغير الجديد. أولاً، لنص السؤال:

<div className= 'question-text' >{questions[currentQuestion].questionText}</div>

وأيضًا لقسم خيارات الإجابة:

<div className= 'answer-section' >
 {questions[currentQuestion].answerOptions.map( ( answerOption, index ) => (
 < button > {answerOption.answerText} </ button >))}
</div>

الآن، إذا قمت بتهيئة currentQuestion بقيمة مختلفة عن 0، مثل 1 أو 2، ستتحدث واجهة المستخدم لعرض السؤال والإجابات الخاصة بذلك السؤال المحدد. هذا رائع!

لنضف بعض الكود بحيث عندما ننقر على إجابة، نقوم بزيادة قيمة currentQuestion للانتقال إلى السؤال التالي. قم بإنشاء دالة جديدة تسمى handleAnswerButtonClick. هذه الدالة هي التي سيتم استدعاؤها عندما ينقر المستخدم على إجابة. سنقوم بزيادة قيمة السؤال الحالي بمقدار واحد، وحفظها في متغير جديد، ثم تعيين هذا المتغير الجديد في الحالة (state):

 const handleAnswerButtonClick = ( answerOption ) => {
 const nextQuestion = currentQuestion + 1 ;
 setCurrentQuestion(nextQuestion);
 };

بعد ذلك، أضف حدث onClick إلى زر الإجابة كما يلي:

<button onClick={ () => handleAnswerButtonClick()}>{answerOption.answerText}</button>

إذا جربت هذا، ستلاحظ أنه يعمل بشكل جيد، حتى نصل إلى نهاية الأسئلة:

خطأ يظهر عند الوصول إلى نهاية الأسئلة في تطبيق React

ما الذي يحدث هنا؟ في دالة handleAnswerButtonClick، نقوم بزيادة الرقم وتعيينه للحالة (state)، وهذا أمر صحيح. لكن تذكر أننا نستخدم هذا الرقم للوصول إلى عناصر مصفوفة questions للحصول على السؤال وخيارات الإجابة. بمجرد أن يصل الرقم إلى 5 (إذا كانت المصفوفة تحتوي على 5 عناصر فقط)، سيحدث خطأ لأن العنصر ذو الفهرس 5 غير موجود!

لنتجنب هذا الخطأ، يجب أن نتحقق من أننا لا نتجاوز حدود المصفوفة. في دالة handleAnswerButtonClick، لنضف الشرط التالي:

 if (nextQuestion < questions.length) {
 setCurrentQuestion(nextQuestion);
 } else {
 alert( 'you reached the end of the quiz' );
 }

هذا الشرط ببساطة يعني: إذا كان رقم السؤال التالي (nextQuestion) أقل من العدد الإجمالي للأسئلة في المصفوفة (questions.length)، فقم بتحديث الحالة إلى السؤال التالي. وإلا، فقد وصلنا إلى نهاية الاختبار، لذا اعرض رسالة تنبيه (alert) في الوقت الحالي.

عرض شاشة النتائج

بدلاً من عرض رسالة تنبيه، ما نريده هو عرض شاشة “النتائج” (Score Screen). إذا نظرت إلى جزء JSX، ستلاحظ أنني قمت بتضمين التنسيق (markup) الخاص بها بالفعل، كل ما نحتاجه هو استبدال القيمة false بالمنطق الصحيح.

كيف نفعل ذلك؟ هذا هو المكان المثالي لوضع كائن حالة جديد! أضف كائن حالة آخر سيخزن ما إذا كنا نريد عرض شاشة النتائج أم لا:

 const [showScore, setShowScore] = useState( false );

واستبدل false بـ showScore في جزء JSX الخاص بك:

<div className= 'app' >{showScore ? < div className = 'score-section' > // ... score section markup </ div > : <> // ... quiz question/answer markup </> }</div>

لن يتغير شيء في البداية، ولكن إذا قمنا بتغيير قيمة الحالة showScore إلى true، فسيتم عرض قسم النتائج (score div). هذا لأن كل شيء ملفوف داخل عامل تشغيل ثلاثي (ternary operator)، مما يعني: “إذا كانت showScore صحيحة (true)، فاعرض تنسيق قسم النتائج، وإلا، فاعرض تنسيق أسئلة/إجابات الاختبار”.

الآن، نريد تحديث متغير الحالة هذا عندما يصل المستخدم إلى نهاية الاختبار. لقد كتبنا بالفعل المنطق الخاص بذلك في دالة handleAnswerButtonClick. كل ما علينا فعله هو استبدال منطق رسالة التنبيه (alert) بتحديث متغير showScore ليصبح true:

 if (nextQuestion < questions.length) {
 setCurrentQuestion(nextQuestion);
 } else {
 setShowScore( true );
 }

إذا قمنا بالنقر على إجابات الاختبار، فسيتم عرض قسم النتائج عندما نصل إلى النهاية. في الوقت الحالي، النص والنتيجة المعروضة هي سلاسل نصية مكتوبة يدويًا (hardcoded strings)، لذا يجب أن نجعلها ديناميكية.

حفظ النقاط (Score)

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

تذكر أنه عندما نكرر على answerOptions، فإن دالة map تعطينا كائنًا لكل خيار إجابة يتضمن questionText، وقيمة منطقية (boolean) توضح ما إذا كانت تلك الإجابة صحيحة أم لا (isCorrect). هذه القيمة المنطقية هي ما سنستخدمه لمساعدتنا في زيادة نقاطنا.

في زر الإجابة الخاص بنا، قم بتحديث الدالة كما يلي:

onClick={ ()=> handleAnswerButtonClick(answerOption.isCorrect)

بعد ذلك، قم بتحديث تعريف الدالة لقبول هذه المعلمة (parameter):

 const handleAnswerButtonClick = ( isCorrect ) => {
 //... other code
 };

الآن يمكننا إضافة بعض المنطق هنا في دالتنا. في الوقت الحالي، نريد أن نقول “إذا كانت isCorrect صحيحة (true)، نريد عرض رسالة تنبيه”:

 const handleAnswerButtonClick = ( isCorrect ) => {
 if (isCorrect) {
 alert(“the answer is correct!”)
 }
 //...other code
 };

هذا مكافئ تمامًا لـ if(isCorrect === true)، ولكنه نسخة مختصرة. الآن إذا جربت هذا، سترى أننا نحصل على رسالة تنبيه عندما ننقر على الإجابة الصحيحة.

لنلخص ما قمنا به حتى الآن:

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

بعد ذلك، نريد حفظ النقاط فعليًا. كيف تعتقد أننا نفعل ذلك؟ إذا قلت “قيمة حالة (state value)” فأنت محق!

امض قدمًا وأضف قيمة حالة أخرى تسمى score. تذكر أن تسبق الدالة التي تغير القيمة بـ set، لتصبح setScore. قم بتهيئتها إلى 0:

 const [score, setScore] = useState( 0 );

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

 const handleAnswerButtonClick = ( isCorrect ) => {
 if (isCorrect) {
 setScore(score + 1 );
 }
 //...other code
 };

عرض النقاط النهائية

لعرض النقاط، نحتاج فقط إلى إجراء تغيير صغير على كود العرض (rendering code) الخاص بنا. في جزء JSX، قم بإزالة السلسلة النصية المكتوبة يدويًا في قسم النقاط، وأضف هذا المتغير الجديد:

<div className= 'score-section' >
 You scored {score} out of {questions.length}
</div>

الآن، إذا قمنا بالمرور عبر إجابات الاختبار، ستكون النتيجة ديناميكية وستعرض بشكل صحيح في النهاية!

تحديث عداد الأسئلة ديناميكيًا

أمر أخير قبل أن نختتم تطبيق الاختبار الخاص بنا: ستلاحظ أن السؤال الحالي المعروض في واجهة المستخدم هو دائمًا “1”، لأنه مكتوب يدويًا. نحتاج إلى تغيير هذا ليكون أكثر ديناميكية. استبدل عنصر div الذي يحمل الفئة question-count بالآتي:

<div className= 'question-count' >
 < span > Question {currentQuestion + 1} </ span >
 /{questions.length}
</div>

تذكر أننا نحتاج إلى إضافة +1 لأن أجهزة الكمبيوتر تبدأ العد من 0 وليس 1.

استكشف المزيد من مشاريع React

هل ترغب في المزيد من أفكار المشاريع لتعزيز مهاراتك في React؟ لا تتردد في بناء المزيد من المشاريع لتوسيع آفاق تعلمك. نحن نقدم أسبوعيًا مشروعًا جديدًا يتضمن مثالًا عمليًا، كودًا أوليًا، ونصائح قيمة. اشترك لتصلك هذه المشاريع مباشرة إلى بريدك الوارد!

إذا وجدت هذه المقالة مفيدة، نرجو منك مشاركتها لدعم المحتوى التقني الهادف.

الخلاصة التقنية

لقد نجحنا في بناء تطبيق اختبار تفاعلي باستخدام React، مستفيدين من قوة إدارة الحالة (State Management) عبر خطاف useState. هذا المشروع يوضح ببراعة كيفية التعامل مع البيانات الديناميكية، التبديل بين المكونات بناءً على شروط معينة (Conditional Rendering)، وتتبع تفاعلات المستخدم لزيادة النقاط. إن فهم هذه المفاهيم الأساسية يمثل حجر الزاوية في بناء تطبيقات React أكثر تعقيدًا وفعالية، ويوفر أساسًا متينًا للمطورين الطموحين.

اترك تعليقاً

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