شرح React: كيفية التعامل مع عدة مربعات اختيار بطريقة احترافية

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

مقدمة: لماذا يختلف التعامل مع Checkbox في React؟

يبدو مربع الاختيار في HTML بسيطاً في الظاهر، لكن عند استخدامه داخل React تصبح طريقة إدارته مختلفة، لأن الواجهة هنا تعتمد على الحالة state وليس على التفاعل المباشر مع العنصر فقط. لهذا السبب، فإن التعامل مع عدة مربعات اختيار يتطلب فهماً جيداً لمفهوم Controlled Input وكيفية ربط كل عنصر بقيمة محفوظة داخل التطبيق.

في هذا الدليل ستتعرّف على طريقة احترافية لبناء واجهة تحتوي على عدة خيارات قابلة للتحديد، مع حساب القيمة الإجمالية للعناصر المحددة. كما ستتعلّم استخدام الدوال map() وreduce() وfill() في سيناريو عملي شائع.

واجهة توضح التعامل مع مربعات الاختيار المتعددة في React

ما الذي ستتعلمه في هذا المقال؟

  • كيفية استخدام مربع اختيار واحد كعنصر إدخال متحكَّم به في React.
  • كيفية إدارة عدة مربعات اختيار عبر مصفوفة داخل state.
  • طريقة الاستفادة من map() لتحديث عنصر واحد داخل المصفوفة.
  • كيفية استخدام reduce() لحساب الإجمالي اعتماداً على العناصر المحددة.
  • إنشاء مصفوفة بطول محدد ومعبأة بقيمة ابتدائية عبر fill().

الخطوة الأولى: التعامل مع مربع اختيار واحد في React

قبل الانتقال إلى الحالة الأكثر تعقيداً، من الأفضل فهم آلية عمل مربع اختيار واحد. في المثال التالي، يتم تعريف عنصر checkbox بالطريقة التقليدية المشابهة لـ HTML:

<div className="App">
  Select your pizza topping:
  <div className="topping">
    <input type="checkbox" id="topping" name="topping" value="Paneer" />
    Paneer
  </div>
</div>

في هذا الشكل، يمكنك تحديد المربع أو إلغاء تحديده، لكن التطبيق لا يملك فهماً حقيقياً لحالته ما لم تكن هذه الحالة مرتبطة بـ state.

مثال يوضح تحديد وإلغاء تحديد مربع اختيار واحد في React

تحويل مربع الاختيار إلى Controlled Input

في React يُفضَّل أن تكون عناصر الإدخال متحكَّماً بها عبر الحالة. هذا يعني أن قيمة العنصر لا تتغير إلا عندما تتغير قيمة state المرتبطة به.

export default function App() {
  const [isChecked, setIsChecked] = useState(false);

  const handleOnChange = () => {
    setIsChecked(!isChecked);
  };

  return (
    <div className="App">
      Select your pizza topping:
      <div className="topping">
        <input
          type="checkbox"
          id="topping"
          name="topping"
          value="Paneer"
          checked={isChecked}
          onChange={handleOnChange}
        />
        Paneer
      </div>
      <div className="result">
        Above checkbox is {isChecked ? "checked" : "un-checked"}.
      </div>
    </div>
  );
}

شرح آلية العمل

تم إنشاء حالة باسم isChecked باستخدام useState() وقيمتها الابتدائية false:

const [isChecked, setIsChecked] = useState(false);

ثم تم تمرير الخاصيتين checked وonChange إلى عنصر الإدخال:

<input ... checked={isChecked} onChange={handleOnChange} />

عند النقر على مربع الاختيار، تُستدعى الدالة handleOnChange وتقوم بعكس القيمة الحالية:

const handleOnChange = () => {
  setIsChecked(!isChecked);
};

إذا كانت القيمة true ستتحول إلى false، وإذا كانت false ستتحول إلى true. وبهذه الطريقة يصبح مربع الاختيار مرتبطاً بالكامل بحالة التطبيق.

لماذا يُنصح باستخدام Controlled Input؟

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

يمكن استخدام ref للتعامل غير المتحكَّم به في حالات محدودة، لكن النمط المتحكَّم به يظل الخيار الأفضل في أغلب تطبيقات React.

كيفية التعامل مع عدة مربعات اختيار في React

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

واجهة تعرض عدة مربعات اختيار مع أسعار العناصر في React

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

إنشاء مصفوفة الحالة باستخدام fill()

لإنشاء مصفوفة بطول يساوي عدد العناصر، يمكن استخدام new Array() مع fill(false):

const [checkedState, setCheckedState] = useState(
  new Array(toppings.length).fill(false)
);

إذا كان لدينا 5 عناصر مثلاً، فستكون القيمة الابتدائية كالتالي:

[false, false, false, false, false]

كل قيمة داخل المصفوفة تمثل حالة مربع اختيار واحد. وعند التحديد أو الإلغاء، تتبدل القيمة المقابلة بين true وfalse.

الكود الكامل للتعامل مع مربعات الاختيار المتعددة

import { useState } from "react";
import { toppings } from "./utils/toppings";
import "./styles.css";

const getFormattedPrice = (price) => `$ ${price.toFixed(2)} `;

export default function App() {
  const [checkedState, setCheckedState] = useState(
    new Array(toppings.length).fill(false)
  );
  const [total, setTotal] = useState(0);

  const handleOnChange = (position) => {
    const updatedCheckedState = checkedState.map((item, index) =>
      index === position ? !item : item
    );

    setCheckedState(updatedCheckedState);

    const totalPrice = updatedCheckedState.reduce(
      (sum, currentState, index) => {
        if (currentState === true) {
          return sum + toppings[index].price;
        }
        return sum;
      },
      0
    );

    setTotal(totalPrice);
  };

  return (
    <div className="App">
      <h3>Select Toppings</h3>
      <ul className="toppings-list">
        {toppings.map(({ name, price }, index) => {
          return (
            <li key={index}>
              <div className="toppings-list-item">
                <div className="left-section">
                  <input
                    type="checkbox"
                    id={`custom-checkbox-${index}`}
                    name={name}
                    value={name}
                    checked={checkedState[index]}
                    onChange={() => handleOnChange(index)}
                  />
                  <label htmlFor={`custom-checkbox-${index}`}>{name}</label>
                </div>
                <div className="right-section">
                  {getFormattedPrice(price)}
                </div>
              </div>
            </li>
          );
        })}
        <li>
          <div className="toppings-list-item">
            <div className="left-section">Total:</div>
            <div className="right-section">
              {getFormattedPrice(total)}
            </div>
          </div>
        </li>
      </ul>
    </div>
  );
}

شرح الكود خطوة بخطوة

ربط كل مربع اختيار بقيمته داخل المصفوفة

كل عنصر checkbox يحصل على قيمة الخاصية checked من الموضع المقابل له داخل المصفوفة checkedState:

<input
  type="checkbox"
  id={`custom-checkbox-${index}`}
  name={name}
  value={name}
  checked={checkedState[index]}
  onChange={() => handleOnChange(index)}
/>

هذا يعني أن المربع الأول يعتمد على checkedState[0]، والثاني على checkedState[1]، وهكذا.

تمرير الفهرس index عند التغيير

عند تغيير أي مربع، يتم إرسال موقعه إلى الدالة handleOnChange():

onChange={() => handleOnChange(index)}

هذا يسمح لنا بمعرفة أي عنصر يجب تحديثه داخل المصفوفة.

تحديث عنصر واحد باستخدام map()

داخل الدالة handleOnChange() يتم إنشاء مصفوفة جديدة عبر map():

const updatedCheckedState = checkedState.map((item, index) =>
  index === position ? !item : item
);

المنطق هنا بسيط:

  • إذا كان index الحالي يساوي position المرسل، نعكس القيمة باستخدام !item.
  • إذا لم يتطابقا، نُبقي القيمة كما هي.

ويمكن كتابة المنطق نفسه بصيغة أكثر تفصيلاً:

const updatedCheckedState = checkedState.map((item, index) => {
  if (index === position) {
    return !item;
  } else {
    return item;
  }
});

استخدام العامل الثلاثي ?: يجعل الكود أقصر، لكن النتيجة واحدة.

أهمية تحديث state بعد إنشاء المصفوفة الجديدة

بعد تجهيز المصفوفة الجديدة، يجب تمريرها إلى setCheckedState():

setCheckedState(updatedCheckedState);

إذا لم تُحدَّث الحالة، فلن ينعكس التغيير على الواجهة، لأن قيمة checked في كل مربع تعتمد مباشرة على checkedState.

لماذا نستخدم updatedCheckedState بدلاً من checkedState؟

هذه نقطة مهمة جداً. تحديث الحالة في React ليس فورياً دائماً، لأن setState أو setCheckedState() يعملان بشكل غير متزامن في كثير من الحالات. لذلك لا يمكن افتراض أن checkedState سيحمل القيمة الجديدة مباشرة في السطر التالي.

لهذا السبب يتم إنشاء متغير وسيط باسم updatedCheckedState واستخدامه مباشرة في الحسابات، لأنه يحتوي بالفعل على القيم المحدّثة.

حساب الإجمالي باستخدام reduce()

بعد تحديث المصفوفة، يتم حساب مجموع الأسعار للعناصر المحددة عبر reduce():

const totalPrice = updatedCheckedState.reduce(
  (sum, currentState, index) => {
    if (currentState === true) {
      return sum + toppings[index].price;
    }
    return sum;
  },
  0
);

تعمل الدالة هنا كالتالي:

  1. تبدأ القيمة التراكمية sum من 0.
  2. تمر على كل عنصر داخل updatedCheckedState.
  3. إذا كانت القيمة الحالية true، يتم جمع سعر العنصر المناظر من toppings[index].price.
  4. إذا كانت false، يستمر المجموع كما هو.

وفي النهاية يتم حفظ النتيجة داخل الحالة total:

setTotal(totalPrice);

حساب السعر الإجمالي عند تحديد عدة مربعات اختيار في React

أفضل الممارسات عند التعامل مع عدة Checkboxes في React

  • استخدم مصفوفة واحدة في state عند وجود عناصر متكررة أو قائمة ديناميكية.
  • تجنب إنشاء useState() مستقل لكل مربع اختيار إذا كانت القائمة طويلة.
  • اعتمد على map() لتوليد الواجهة وتحديث البيانات بطريقة أنظف.
  • أنشئ نسخة جديدة من المصفوفة بدلاً من تعديلها مباشرة، التزاماً بمبدأ عدم تغيير البيانات الأصلية immutability.
  • استخدم reduce() عندما تحتاج إلى حسابات مشتقة مثل المجموع أو العدد أو القيم المفلترة.

متى تكون هذه الطريقة مناسبة؟

يُعد هذا الأسلوب مناسباً في حالات كثيرة، مثل:

  • نماذج اختيار الخدمات أو الإضافات المدفوعة.
  • واجهات تصفية المنتجات حسب الخصائص.
  • تحديد الصلاحيات في لوحات التحكم.
  • اختيار مجموعة عناصر مع حساب إجمالي أو عدد نهائي.

ملاحظات تقنية لتحسين جودة الكود

إذا كنت تبني مشروعاً أكبر، يمكنك لاحقاً تطوير هذا النمط عبر:

  • استخدام useMemo لحساب الإجمالي عند الحاجة إلى تحسين الأداء.
  • فصل عنصر مربع الاختيار في مكوّن مستقل إذا أصبحت الواجهة كبيرة.
  • الاعتماد على معرف فريد بدلاً من index إذا كانت القائمة قابلة لإعادة الترتيب أو الحذف.

دورة تعليمية متقدمة حول Redux وتطوير تطبيقات React العملية

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

إدارة عدة مربعات اختيار في React تصبح سهلة ومنظمة عندما تتعامل معها كبيانات داخل state بدلاً من اعتبارها عناصر واجهة منفصلة فقط. استخدام مصفوفة من القيم المنطقية مع map() للتحديث وreduce() للحساب يوفّر حلاً واضحاً وقابلاً للتوسع. تقنياً، هذه المقاربة تُعد من أفضل الأساليب لبناء واجهات تفاعلية دقيقة وسهلة الصيانة، خصوصاً في التطبيقات التي تعتمد على القوائم الديناميكية والحسابات الفورية.

اترك تعليقاً

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