شرح Store و Actions و Reducers في Redux

شرح Store و Actions و Reducers في Redux

مقدمة شرح Store و Actions و Reducers

إذا كنت قد بدأت تتعامل مع React أو دخلت إلى عالم بناء التطبيقات الحديثة من جهة الواجهة الأمامية، فغالبًا ستصل في لحظة ما إلى السؤال الذي يربك كثيرًا من المطورين في البداية: كيف أُدير الحالة المشتركة بين مكوّنات كثيرة بطريقة منظمة، قابلة للتوسع، وسهلة التتبع؟ هنا يظهر Redux ليس كأداة “إضافية” فقط، بل كطريقة تفكير كاملة تساعدك على جعل تدفق البيانات في التطبيق واضحًا مثل الطريق المستقيم، بدل أن يتحول إلى شبكة معقدة من تمرير الخصائص وتكرار المنطق وتضخم الحالة داخل كل مكوّن.

Redux في جوهره بسيط جدًا، لكن قيمته الحقيقية تأتي من انضباطه. هو يقول لك: ضع الحالة في مكان واحد، ولا تسمح لأي جزء من التطبيق أن يعبث بها كيفما شاء، بل اجعل كل تغيير يمر عبر خطوات مفهومة يمكن تتبعها. وهذه الخطوات الثلاث التي سنفككها هنا بالتفصيل هي: Store و Actions و Reducers. هذه المصطلحات تبدو في البداية نظرية أو ثقيلة، لكنك عندما تفهمها جيدًا ستكتشف أنها تشبه جدًا طريقة تنظيم العمل في الحياة اليومية: هناك مكان مركزي لحفظ المعلومات، وهناك رسائل تطلب التغيير، وهناك جهة مسؤولة عن تنفيذ هذا التغيير وفق قواعد واضحة. وكلما كان التطبيق أكبر، زادت أهمية هذا التنظيم، لأن الفوضى الصغيرة اليوم قد تتحول غدًا إلى تعقيد يستهلك وقتك وعقلك.

لماذا نحتاج Redux أصلًا؟

قبل أن نفهم Store و Actions و Reducers، من المهم أن نفهم المشكلة التي جاء Redux ليعالجها. كثير من التطبيقات الصغيرة تبدأ بشكل جميل وبسيط: مكوّن يعرض بيانات، ومكوّن آخر يغيرها، وثالث يتابع النتيجة. لكن مع الوقت، يبدأ التطبيق في النمو: صفحة تسجيل دخول، سلة مشتريات، نظام تعليقات، إعدادات مستخدم، إشعارات، صلاحيات، بيانات يتم جلبها من API، حالات تحميل وأخطاء، ونوافذ منبثقة، وفلترة، وترقيم صفحات، وقوائم كثيرة مرتبطة ببعضها. هنا يصبح الاعتماد فقط على state المحلية لكل مكوّن أمرًا مرهقًا جدًا، لأنك ستجد نفسك تنقل البيانات من مستوى إلى مستوى، وتمريرها عبر props بشكل متسلسل، ثم تكرر نفس منطق التحديث في أكثر من مكان.

Redux يخفف هذا العبء عبر فكرة واضحة جدًا: اجعل الحالة العامة للتطبيق في مستودع مركزي، واسمح بتغييرها عبر أوامر واضحة، واحتفظ بمنطق التغيير في مكان واحد. هكذا يصبح تتبع ما حدث أسهل، وتصحيح الأخطاء أسرع، واختبار المنطق أبسط، والتوسع أقل إيلامًا. ليس هذا فقط، بل إن التنظيم الذي يفرضه Redux يجعلك تفكر في التطبيق بمنطق “ماذا حدث؟” بدل “أين أضع هذا التغيير؟”. هذا التحول في التفكير وحده كفيل بأن يرفع جودة الكود بشكل ملحوظ.

الصورة الكبيرة: كيف يعمل Redux؟

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

  1. Store هو المكان المركزي الذي يحتفظ بحالة التطبيق.

  2. Action هو كائن يصف ما الذي حدث أو ما الذي تريد حدوثه.

  3. Reducer هي الدالة التي تستقبل الحالة الحالية والإجراء، ثم تعيد حالة جديدة.

قد يبدو الأمر في سطر واحد بسيطًا جدًا، لكن كل كلمة هنا مهمة. Redux لا يقول لك “غيّر الحالة مباشرة”، بل يقول: “صف الحدث، ثم دع reducer يقرر الشكل الجديد للحالة”. هذه الفلسفة تجعل كل تحديث قابلًا للتتبع، لأنك لا تضيف قيمًا عشوائية إلى الذاكرة، بل تمر عبر سلسلة واضحة يمكن مراقبتها.

ما هو Store في Redux؟

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

في التطبيقات التقليدية بدون Redux، قد تجد الحالة موزعة بين عدة مكونات. مكوّن يحفظ اسم المستخدم، وآخر يحفظ عدد العناصر في السلة، وثالث يحفظ حالة ظهور modal، ورابع يتابع نتائج البحث. هذا التوزع قد يكون مقبولًا في المشاريع الصغيرة، لكنه يصبح صعبًا عندما تتداخل الحالات. أما في Redux، فهناك فكرة أوضح: احفظ الحالة العامة في Store، واجعل المكوّنات تقرأ منه وتطلب التغيير عبر actions.

من المهم أن نفهم أن الـ Store ليس مجرد متغير كبير. هو كائن غني يوفر لك أدوات للتعامل مع الحالة، مثل قراءة القيمة الحالية، إرسال action، والاشتراك في التحديثات. وفي Redux الحديث، غالبًا لن تتعامل مع الـ Store يدويًا كثيرًا كما كان الأمر قديمًا، لأن أدوات مثل @reduxjs/toolkit جعلت الاستخدام أسهل وأكثر أمانًا، لكن الفكرة الجوهرية بقيت كما هي.

ماذا يحتوي الـ Store؟

الـ Store عادة يحتوي على شجرة كاملة من الحالة. قد تكون الحالة مثلًا بهذا الشكل:

{
  auth: {
    user: null,
    token: null,
    isLoggedIn: false
  },
  cart: {
    items: [],
    total: 0
  },
  ui: {
    loading: false,
    modalOpen: false
  }
}

هذا يعني أن Store ليس “شيئًا واحدًا مسطحًا” بالضرورة، بل يمكن أن يحتوي على أجزاء متعددة، وكل جزء يمثل domain معين داخل التطبيق. هذا التقسيم مهم جدًا، لأنه يجعل الإدارة أسهل. بدل أن يكون لديك reducer واحد ضخم يدير كل شيء، يمكنك تقسيم التطبيق إلى slices أو أقسام.

لماذا الـ Store مهم جدًا؟

لأن وجود مصدر واحد للحقيقة يزيل كثيرًا من الالتباس. عندما يكون هناك أكثر من نسخة من البيانات نفسها في أماكن مختلفة، تبدأ المشاكل: هذه النسخة محدثة، وتلك قديمة، وهذه فُقدت أثناء rerender، وتلك لم تصل إليها آخر API response. أما عندما تكون الحقيقة في مكان واحد، يصبح كل شيء أبسط. المكوّنات لا تختلق الحقيقة من عندها، بل تستخرجها من المصدر المركزي.

ومع ذلك، لا يعني هذا أن كل شيء يجب أن يوضع في الـ Store. هذه نقطة مهمة جدًا ويخطئ فيها كثير من المبتدئين. ليس كل state يحتاج Redux. الحالة المحلية البسيطة مثل قيمة input مؤقتة أو حالة hover أو فتح dropdown صغير قد تكون أفضل داخل المكوّن نفسه. Redux مناسب أكثر للحالة المشتركة، أو الحالة التي تحتاج أن تُقرأ من أماكن متعددة، أو التي يجب أن تبقى قابلة للتتبع والتاريخ والتوسع.

ما هي Actions؟

Action هي كائن بسيط يصف حدثًا حدث داخل التطبيق. هذا الحدث قد يكون “أضف عنصرًا إلى السلة”، أو “سجّل دخول المستخدم”، أو “غيّر اللغة”، أو “أظهر رسالة خطأ”، أو “احذف تعليقًا”. الفكرة هنا أن الـ action لا تنفذ التغيير بنفسها، بل تصفه فقط. إنها رسالة، لا قرار.

أبسط شكل للـ action هو كائن يحتوي عادة على type، وأحيانًا payload. مثلًا:

{
  type: "counter/increment"
}

أو:

{
  type: "cart/addItem",
  payload: {
    id: 1,
    name: "Laptop",
    price: 1200
  }
}

الـ type هو اسم الحدث، وهو الجزء الأهم لأن reducer يعتمد عليه ليعرف ماذا يفعل. أما payload فهو البيانات المرافقة لهذا الحدث. أحيانًا تكون هناك حقول أخرى مثل meta أو error، لكن الفكرة الأساسية هي أن الـ action تصف ما الذي حدث أو ما الذي نريد أن يحدث.

لماذا نحتاج Action بدل التعديل المباشر؟

لأن Redux يعتمد على تدفق منظم: حدث -> Action -> Reducer -> State جديدة -> تحديث الواجهة. لو كنت تعدل الحالة مباشرة، ستفقد هذا الانضباط، وسيصعب تتبع من غيّر ماذا ومتى ولماذا. أما حين تستخدم action، فإنك تكتب سجلًا واضحًا للأحداث. هذا مفيد جدًا في debugging، وفي فهم التطبيق، وفي بناء بنية يمكن لعدة مطورين العمل عليها دون أن يتداخلوا بشكل فوضوي.

Action ليست منطقًا

من الأخطاء الشائعة أن يضع المطور منطقًا داخل action. لكن action في الفلسفة الكلاسيكية لـ Redux هي مجرد وصف للحدث، لا مكانًا للتنفيذ المعقد. المنطق الحقيقي، من حيث تحديث الحالة، يعيش غالبًا في reducer أو في middleware أو في thunks/sagas عند الحاجة. وهذا الفصل بين الوصف والتنفيذ هو سر قوة Redux.

مثال بسيط على Action

const addTodoAction = {
  type: "todos/addTodo",
  payload: {
    id: 1,
    text: "تعلم Redux"
  }
};

هذه الرسالة وحدها لا تغيّر شيئًا. هي مجرد رسالة تقول: هناك شيء اسمه إضافة مهمة جديدة. من سيقرر كيف تُضاف؟ الـ reducer.

ما هو Reducer؟

إذا كان الـ action هو الرسالة، فالـ Reducer هو الجهة التي تقرأ الرسالة وتقرر كيف يجب أن تتغير الحالة. Reducer هي دالة تستقبل حالتين أساسيتين: state الحالية و action، ثم تعيد state جديدة.

الشكل الشائع لها يكون هكذا:

function reducer(state, action) {
  switch (action.type) {
    case "some/action":
      return newState;
    default:
      return state;
  }
}

الاسم Reducer جاء من فكرة “الاختزال” أو “الدمج” أو “التحويل” إلى حالة جديدة اعتمادًا على المدخلات السابقة. وهي دالة يجب أن تكون في الغالب pure function، أي أن نتيجتها تعتمد فقط على المدخلات، ولا تقوم بآثار جانبية، ولا تعدل الحالة الأصلية مباشرة.

لماذا Reducer مهم جدًا؟

لأن reducer هو قلب Redux الحقيقي. هو المكان الذي تحدد فيه قواعد اللعبة. عندما يصل action معين، ماذا يجب أن يحدث؟ إذا كان action هو “أضف عنصرًا”، كيف تزيد العدد؟ إذا كان “احذف عنصرًا”، كيف تزيله من القائمة؟ إذا كان “غيّر اسم المستخدم”، كيف يتم التحديث؟ كل هذه القرارات تتم داخل reducer أو من خلال reducers المتعددة.

الخصائص الأساسية للـ Reducer الجيد

Reducer الجيد يجب أن يكون:

  • قابلًا للتنبؤ: نفس المدخلات تعطي نفس المخرجات.

  • خاليًا من الآثار الجانبية: لا يذهب مباشرة إلى API، ولا يكتب في localStorage عادة، ولا يغير DOM.

  • غير متلف للحالة الأصلية: لا تعدل state القديمة مباشرة.

  • واضحًا ومقسّمًا: كل reducer يعالج جزءًا منطقيًا من الحالة.

هذا مهم جدًا لأن التنبؤ هو ما يجعل Redux قويًا. عندما يكون reducer نقيًا وواضحًا، يصبح من السهل جدًا اختبار السلوك، ومعرفة سبب حدوث كل تحديث.

العلاقة بين Store و Actions و Reducers

العلاقة بينهم هي العمود الفقري لـ Redux. يمكن تلخيصها في سلسلة واحدة:

المكوّن يرسل Action → الـ Store يستقبلها → الـ Reducer يعالجها → State جديدة تُخزن → المكوّنات تتحدث تلقائيًا

هذا التدفق هو ما يجعل كل شيء مرتبًا. لا يوجد عبث عشوائي بالحالة. لا يوجد مكوّن يقرر بنفسه تغيير الحالة المركزية بدون المرور بالقواعد. وحتى إن بدا هذا مقيدًا في البداية، فستكتشف لاحقًا أن هذا “القيد” هو الذي يعطيك الحرية الحقيقية في التوسع دون خوف.

مثال عملي بسيط: عداد

لنرَ مثالًا صغيرًا جدًا يوضح الفكرة من البداية إلى النهاية.

تعريف الحالة الأولى

const initialState = {
  value: 0
};

تعريف reducer

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case "counter/increment":
      return {
        ...state,
        value: state.value + 1
      };
    case "counter/decrement":
      return {
        ...state,
        value: state.value - 1
      };
    default:
      return state;
  }
}

إنشاء actions

const incrementAction = {
  type: "counter/increment"
};

const decrementAction = {
  type: "counter/decrement"
};

كيف يحدث التغيير؟

عندما يرسل التطبيق incrementAction، فإن reducer يلتقطه، يرى أن النوع هو counter/increment، ثم يعيد state جديدة يزيد فيها value بمقدار واحد. وإذا أُرسل decrementAction، يقلل القيمة بمقدار واحد. هذه البساطة الشديدة هي ما يجعل Redux مفهومًا عندما تراه في مثال صغير.

لكن الجميل هنا أن هذا المثال الصغير هو نفسه النموذج الذي يكبر ليخدم تطبيقًا ضخمًا. الفرق فقط أن الحالة تصبح أعقد، والـ reducers أكثر عددًا، وربما نستخدم أدوات مساعدة مثل Redux Toolkit.

لماذا لا نعدّل الـ state مباشرة؟

هذه واحدة من أهم الأفكار في Redux، وربما أكثر فكرة تثير أسئلة في البداية. لماذا لا نفعل هكذا؟

state.value = state.value + 1;

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

هذا لا يعني أنك لن ترى أبداً كودًا يبدو فيه كأنه “تعديل”، لأن Redux Toolkit يستخدم Immer داخليًا ليمنحك كتابة تبدو تعديلية بينما هي في الواقع تنتج نسخًا آمنة. لكن من الناحية المفاهيمية، الفكرة ما زالت: لا تمسّ الأصل مباشرة.

كيف يمر الحدث داخل Redux؟

لنأخذ مثالًا واقعيًا من تطبيق متجر إلكتروني. المستخدم ضغط على زر “إضافة إلى السلة”. ماذا يحدث؟

  1. المكوّن يرسل action من نوع cart/addItem.

  2. الـ store يمرر هذا action إلى reducer المناسب.

  3. reducer يقرأ الـ payload مثل معلومات المنتج.

  4. reducer ينشئ state جديدة فيها المنتج مضافًا إلى القائمة.

  5. الواجهة تعيد الرسم، فيظهر المنتج داخل السلة.

لاحظ أن المكوّن لم يقرر كيف يضاف المنتج إلى السلة. هو فقط قال: “هناك حدث إضافة”. أما المنطق الحقيقي، مثل هل هذا المنتج موجود مسبقًا؟ هل نزيد الكمية؟ هل نضيفه كعنصر جديد؟ فهذه كلها قواعد تعيش في reducer أو في طبقة منطق مرافقة.

ما الفرق بين Action و Reducer؟

من أكثر الأسئلة الشائعة لدى المبتدئين: ما الفرق بينهما تحديدًا؟ الإجابة القصيرة: Action يصف ما حدث، وReducer يقرر كيف تتغير الحالة بناءً عليه. لكن دعنا نفككها أكثر.

الـ action مثل الرسالة التي تقول: “تم الضغط على زر الحفظ”. هذه الرسالة لا تحفظ شيئًا. أما الـ reducer فهو الذي يقول: حسنًا، عندما يصلني هذا النوع من الرسالة، سأحدث الحالة بهذه الطريقة. يمكنك اعتبار الـ action سببًا، وreducer نتيجة منطقية تتبع هذا السبب.

وهنا تظهر نقطة مهمة: لا تجعل الـ reducer يعرف كل شيء عن الواجهة، ولا تجعل الـ action يحمل منطقًا معقدًا. الهدف هو الفصل النظيف بين المعنى والتنفيذ. هذا الفصل هو ما يعطيك بنية يفهمها أي مطور يدخل على الكود بعدك.

مثال عملي أكبر: سلة مشتريات

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

initial state

const initialState = {
  items: [],
  totalQuantity: 0,
  totalPrice: 0
};

reducer

function cartReducer(state = initialState, action) {
  switch (action.type) {
    case "cart/addItem": {
      const newItem = action.payload;
      const existingItem = state.items.find(item => item.id === newItem.id);

      let updatedItems;
      let updatedQuantity = state.totalQuantity;
      let updatedPrice = state.totalPrice;

      if (existingItem) {
        updatedItems = state.items.map(item =>
          item.id === newItem.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      } else {
        updatedItems = [...state.items, { ...newItem, quantity: 1 }];
      }

      updatedQuantity += 1;
      updatedPrice += newItem.price;

      return {
        ...state,
        items: updatedItems,
        totalQuantity: updatedQuantity,
        totalPrice: updatedPrice
      };
    }

    case "cart/removeItem": {
      const itemId = action.payload.id;
      const itemToRemove = state.items.find(item => item.id === itemId);

      if (!itemToRemove) return state;

      return {
        ...state,
        items: state.items.filter(item => item.id !== itemId),
        totalQuantity: state.totalQuantity - itemToRemove.quantity,
        totalPrice: state.totalPrice - itemToRemove.price * itemToRemove.quantity
      };
    }

    case "cart/clear":
      return initialState;

    default:
      return state;
  }
}

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

لماذا تقسيم الـ Redux إلى أجزاء فكرة ممتازة؟

عندما تبدأ مشروعًا بسيطًا، قد تميل إلى وضع كل شيء في reducer واحد. هذا ممكن، لكنه غالبًا لا يدوم طويلًا. مع الوقت، سيتحول reducer إلى ملف طويل جدًا، مليء بـ cases كثيرة، وسيصبح فهمه صعبًا وصيانته أثقل. لذلك من الأفضل تقسيم الحالة إلى slices أو reducers مستقلة حسب المجال.

مثلًا:

  • authReducer لإدارة تسجيل الدخول والمستخدم.

  • cartReducer لإدارة السلة.

  • uiReducer لإدارة النوافذ المنبثقة والتحميل.

  • productsReducer لإدارة المنتجات والفلترة.

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

ماذا يعني أن الـ Reducer يجب أن يكون pure function؟

الدالة النقية أو pure function هي دالة تعتمد على المدخلات فقط، ولا تغيّر شيئًا خارج نطاقها، وتنتج نفس المخرجات لنفس المدخلات. في Redux، هذا مهم للغاية لأننا نريد نتائج قابلة للتنبؤ. إذا أرسلت نفس الحالة ونفس action، يجب أن نحصل على نفس النتيجة.

مثال على reducer نقي:

function sumReducer(state = 0, action) {
  switch (action.type) {
    case "sum/add":
      return state + action.payload;
    default:
      return state;
  }
}

لكن هذا ليس مناسبًا:

function badReducer(state = 0, action) {
  if (action.type === "sum/add") {
    console.log("Updating state");
    return state + Math.random();
  }
  return state;
}

هنا أصبحنا نستخدم Math.random()، وبالتالي نفس المدخلات لن تعطي نفس النتيجة. هذا يضرب جوهر Redux في الصميم. كذلك طباعة logs داخل reducer قد تكون مقبولة أحيانًا لأغراض التعليم، لكن المنهجية الصحيحة هي إبقاء reducer نظيفًا قدر الإمكان.

كيف يتعامل React مع Redux؟

غالبًا ستستخدم Redux داخل تطبيق React عبر مكتبة react-redux. هذه المكتبة تربط بين الـ Store والمكوّنات، بحيث يمكن للمكوّنات قراءة state وإرسال actions بشكل مريح. في الاستخدام الحديث، غالبًا تعتمد على useSelector لقراءة جزء من الحالة، وuseDispatch لإرسال actions.

مثال:

import { useSelector, useDispatch } from "react-redux";

function Counter() {
  const value = useSelector(state => state.counter.value);
  const dispatch = useDispatch();

  return (
    <div>
      <p>{value}</p>
      <button onClick={() => dispatch({ type: "counter/increment" })}>
        زيادة
      </button>
    </div>
  );
}

هذا المثال الصغير يظهر الفكرة الجوهرية: المكوّن يقرأ من الـ Store، ويطلق action عند الحاجة. لا حاجة لتخزين نفس القيمة في أماكن متعددة. لا حاجة لتمريرها عبر props من الأعلى إلى الأسفل إذا لم يكن ذلك ضروريًا.

Redux Toolkit: لماذا أصبح الجميع تقريبًا يفضله؟

إذا سمعت عن Redux قديمًا، فقد تكون صادفت كودًا طويلًا من action types، action creators، switch statements، وربما ملف إعداد معقد بعض الشيء. Redux Toolkit جاء ليحل كثيرًا من هذه التعقيدات ويجعل الاستخدام أكثر عملية وأقل قابلية للخطأ. هو ليس بديلًا لفكرة Redux، بل هو الطريقة الرسمية الحديثة لاستخدامه بسهولة أكبر.

Redux Toolkit يقدم لك أدوات مثل:

  • configureStore

  • createSlice

  • createAsyncThunk

هذه الأدوات تختصر الكثير من الكود وتمنع الأخطاء الشائعة. مثلًا، createSlice يسمح لك بكتابة reducers وactions في مكان واحد، ويولّد action creators تلقائيًا. وهذا رائع لأنه يجعل الكود أقل ازدحامًا وأكثر وضوحًا.

مثال:

import { createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    increment(state) {
      state.value += 1;
    },
    decrement(state) {
      state.value -= 1;
    }
  }
});

export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;

قد يبدو هذا كأنه تعديل مباشر، لكن Redux Toolkit يستخدم Immer تحت الغطاء ليحوّل هذا الأسلوب إلى تحديث آمن وغير مباشر. هذا يجعل كتابة reducers أسهل بكثير، خاصة للمبتدئين، ويقلل أخطاء النسخ والانتشار ....

كيف تعمل Actions وReducers مع البيانات غير المتزامنة؟

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

الفكرة العامة هي أن action يمكن أن تعبّر عن:

  • بدء التحميل

  • نجاح التحميل

  • فشل التحميل

مثال:

{
  type: "users/fetchStart"
}
{
  type: "users/fetchSuccess",
  payload: users
}
{
  type: "users/fetchError",
  payload: "حدث خطأ أثناء جلب البيانات"
}

الـ reducer هنا لا يجلب البيانات بنفسه، بل يحدّث الحالة حسب المرحلة الحالية. مثلًا:

const initialState = {
  data: [],
  loading: false,
  error: null
};

function usersReducer(state = initialState, action) {
  switch (action.type) {
    case "users/fetchStart":
      return { ...state, loading: true, error: null };
    case "users/fetchSuccess":
      return { ...state, loading: false, data: action.payload };
    case "users/fetchError":
      return { ...state, loading: false, error: action.payload };
    default:
      return state;
  }
}

هذا النموذج بسيط، لكنه يمثل طريقة التفكير الصحيحة. الـ reducer لا يعرف تفاصيل الطلب الشبكي، بل فقط يعرف كيف تبدو الحالة في كل مرحلة.

أخطاء شائعة يقع فيها المبتدئون

عند تعلم Redux، هناك أخطاء متكررة تظهر كثيرًا، ومن المفيد جدًا أن تنتبه لها مبكرًا.

أول خطأ هو استخدام Redux لكل شيء. ليس كل state يحتاج Store عالمي. إذا كانت الحالة محلية جدًا ولا تشاركها مكوّنات أخرى، فغالبًا state محلية أفضل.

الخطأ الثاني هو تعديل الحالة مباشرة داخل reducer. هذا يفسد مبدأ الـ immutability ويجعل السلوك غير مضمون.

الخطأ الثالث هو وضع منطق business ضخم داخل components. الأفضل أن تكون المكوّنات مسؤولة عن العرض والتفاعل، بينما يبقى منطق الحالة في Redux.

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

الخطأ الخامس هو الخلط بين state UI مؤقتة وstate تطبيقية مهمة. ليس كل شيء يجب أن يعيش في Store العالمي.

متى يكون Redux هو الاختيار المناسب؟

Redux ليس العلاج السحري لكل مشروع. هناك مشاريع صغيرة أو متوسطة قد لا تحتاجه أصلًا. لكن Redux يصبح ذا قيمة كبيرة عندما:

  • تكون الحالة مشتركة بين عدة مكونات أو صفحات.

  • تحتاج إلى تتبع التغييرات بشكل واضح.

  • يكون لديك منطق معقد أو يعتمد على حالات متعددة.

  • تعمل ضمن فريق وتحتاج إلى بنية موحدة.

  • تريد سهولة في الاختبار والتصحيح.

أما إذا كان التطبيق بسيطًا جدًا، فقد يكون استخدام React state أو Context كافيًا. المهم ليس أن تستخدم Redux لمجرد أنه مشهور، بل أن تستخدمه عندما يضيف قيمة فعلية.

لماذا يسهل اختبار Redux؟

واحدة من أجمل ميزات Redux أن الـ reducers سهلة الاختبار للغاية. بما أن reducer هو دالة نقية، فيمكنك تمرير state وaction، ثم التأكد من أن الناتج صحيح. لا حاجة لمحاكاة DOM كامل أو تفاعل معقد في كل مرة.

مثال اختبار بسيط:

test("should increment value", () => {
  const initialState = { value: 0 };
  const action = { type: "counter/increment" };

  const nextState = counterReducer(initialState, action);

  expect(nextState.value).toBe(1);
});

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

Redux كفلسفة تنظيمية قبل أن يكون مكتبة

من الجميل أن ننظر إلى Redux ليس فقط كمكتبة، بل كطريقة تفكير. الفكرة الأساسية التي يروج لها هي أن تدفق البيانات يجب أن يكون واضحًا، وأن التغييرات يجب أن تمر عبر قناة معروفة، وأن الحالة يجب أن تبقى قابلة للتتبع والتنبؤ. هذه الفلسفة مفيدة حتى لو لم تستخدم Redux حرفيًا في كل مشروع. كثير من المطورين يفهمون Redux ثم يكتشفون أن طريقة تفكيرهم في التصميم تغيّرت للأفضل.

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

مثال كامل مبسط باستخدام Redux Toolkit

لنربط الأمور معًا في مثال أكثر واقعية لكنه ما يزال بسيطًا. لنفترض أن لدينا تطبيق ملاحظات.

notesSlice.js

import { createSlice } from "@reduxjs/toolkit";

const notesSlice = createSlice({
  name: "notes",
  initialState: {
    items: []
  },
  reducers: {
    addNote(state, action) {
      state.items.push({
        id: Date.now(),
        text: action.payload
      });
    },
    removeNote(state, action) {
      state.items = state.items.filter(note => note.id !== action.payload);
    },
    clearNotes(state) {
      state.items = [];
    }
  }
});

export const { addNote, removeNote, clearNotes } = notesSlice.actions;
export default notesSlice.reducer;

store.js

import { configureStore } from "@reduxjs/toolkit";
import notesReducer from "./notesSlice";

const store = configureStore({
  reducer: {
    notes: notesReducer
  }
});

export default store;

NotesComponent.jsx

import { useDispatch, useSelector } from "react-redux";
import { addNote, removeNote, clearNotes } from "./notesSlice";
import { useState } from "react";

export default function NotesComponent() {
  const [text, setText] = useState("");
  const notes = useSelector(state => state.notes.items);
  const dispatch = useDispatch();

  const handleAdd = () => {
    if (text.trim()) {
      dispatch(addNote(text));
      setText("");
    }
  };

  return (
    <div>
      <h2>الملاحظات</h2>
      <input
        value={text}
        onChange={e => setText(e.target.value)}
        placeholder="اكتب ملاحظة"
      />
      <button onClick={handleAdd}>إضافة</button>
      <button onClick={() => dispatch(clearNotes())}>مسح الكل</button>

      <ul>
        {notes.map(note => (
          <li key={note.id}>
            {note.text}
            <button onClick={() => dispatch(removeNote(note.id))}>
              حذف
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

هنا نرى الفكرة كاملة:

  • الـ store يحوي state.

  • الـ slice يحتوي actions وreducers.

  • المكوّن يقرأ البيانات من الـ store.

  • المكوّن يرسل actions عند التفاعل.

  • reducer يقرر كيف تتغير الحالة.

هذا النموذج صغير، لكنه يختصر روح Redux كلها.

كيف تفكر عند تصميم Redux في مشروع حقيقي؟

عند بناء تطبيق حقيقي، لا تبدأ بكتابة الأكواد مباشرة. ابدأ بطرح أسئلة تنظيمية:

  • ما هي الحالة المشتركة فعلًا؟

  • ما الذي يحتاج أن يبقى في الذاكرة بين الصفحات؟

  • ما الذي يجب أن يكون متاحًا لعدة مكونات؟

  • ما الذي يمثل “مصدر الحقيقة”؟

  • ما هي الأحداث الأساسية التي تغيّر البيانات؟

بعد ذلك، قسم الحالة إلى domains منطقية. لا تضع كل شيء في ملف واحد. اجعل كل slice مسؤولًا عن جزء محدد. ثم صغ actions بأسماء واضحة تشير إلى الحدث، مثل userLoggedIn, itemAdded, filterChanged, modalOpened. كلما كانت الأسماء واضحة، أصبح الكود يفهم نفسه تقريبًا.

قوة الأسماء الواضحة

قد تبدو التسمية مسألة صغيرة، لكنها في Redux مهمة جدًا. لأن actions هي أحداث، وأسماء الأحداث يجب أن تكون معبرة. بدل أن تسمي action بشكل غامض مثل UPDATE_DATA، من الأفضل أن تستخدم اسمًا دقيقًا مثل profile/updateAvatar أو cart/removeItem. هذا يساعدك أنت وفريقك على فهم المقصود بسرعة، ويجعل الـ logs والمراقبة أكثر معنى.

نفس الأمر ينطبق على reducers. لا تجعل اسمها عامًا جدًا. إذا كان reducer يدير السلة، فليكن متعلقًا بالسلة. إذا كان slice يتعلق بالمصادقة، فليكن واضحًا أنه مسؤول عن auth. التنظيم هنا ليس رفاهية، بل عامل إنتاجية كبير.

ماذا عن الأداء؟

سؤال شائع أيضًا: هل Redux بطيء؟ في الغالب لا، ليس عندما يستخدم بشكل صحيح. Redux مصمم ليكون متوقعًا ومنظمًا، وReact-Redux يساعد في تقليل إعادة الرندر غير الضرورية عند استخدام selectors بشكل مناسب. المشكلة عادة لا تكون في Redux نفسه، بل في التصميم السيئ للحالة أو في اختيار أجزاء غير مناسبة لتعيش في الـ store.

إذا أردت أداءً جيدًا، فاحرص على:

  • اختيار state مناسبة للـ store.

  • عدم تخزين أشياء غير ضرورية عالميًا.

  • استخدام selectors بعناية.

  • تقسيم الحالة منطقيًا.

  • تجنب تحديثات عشوائية واسعة النطاق.

متى تتجه إلى Context بدل Redux؟

Context أداة ممتازة، لكنها ليست بديلًا مثاليًا في كل حالة. إذا كانت الحالة بسيطة ومحدودة مثل theme أو locale أو auth بسيط جدًا، فقد يكون Context كافيًا. أما إذا كانت الحالة معقدة، كثيرة الأحداث، أو تحتاج إلى منطق غني وتاريخ واضح، فغالبًا Redux أفضل.

القاعدة العملية الجميلة هي: استخدم الأداة الأقل تعقيدًا التي تحقق المطلوب. لا تجعل المشروع أثقل من حاجته. لكن أيضًا لا تتردد في استخدام Redux عندما تصبح الفوضى محتملة. الخبرة هنا تعني التوازن، لا التحيز الأعمى لأداة معينة.

نظرة واقعية من تجربة المطور

كثير من المطورين عندما يتعلمون Redux لأول مرة يشعرون أنه “كثير الإجراءات من أجل قيمة واحدة”. ثم بعد أيام أو أسابيع، عندما يبدأ التطبيق في النمو، يفاجأون بأن كل تلك الإجراءات كانت تحميهم من الفوضى. هذه التجربة شائعة جدًا. Redux قد يبدو رسميًا أكثر من اللازم في البداية، لكنه مع الوقت يُظهر فائدته: كل شيء في مكانه، وكل تغيير له مسار واضح، وكل خطأ يمكن تتبعه.

وهنا تكمن الجاذبية الحقيقية لـ Redux. ليس لأنه يجعل الكود أقل، بل لأنه يجعله أكثر قابلية للفهم. وفي المشاريع البرمجية، الفهم هو العملة الأغلى.

خلاصة الفكرة

يمكن تلخيص الصورة النهائية هكذا:
Store هو المكان المركزي الذي يحفظ حالة التطبيق.
Action هو الوصف لما حدث أو لما نريده أن يحدث.
Reducer هو الدالة التي تقرر كيف تتحول الحالة القديمة إلى حالة جديدة بناءً على هذا الحدث.

عندما تفهم هذه الثلاثية، فأنت لم تعد “تستخدم Redux فقط”، بل بدأت تفهم فلسفته. وستجد أن كثيرًا من الكود الذي بدا غامضًا في البداية صار منطقيًا جدًا. ستعرف لماذا يرسل المكوّن action بدل التعديل المباشر، ولماذا reducer يجب أن يكون نقيًا، ولماذا الحالة المركزية تساعد في بناء تطبيقات أكبر وأكثر استقرارًا.

Redux ليس سحرًا، لكنه نظام. والنظام في البرمجة نعمة كبيرة، لأنه يترك لك مساحة للتركيز على منطق المنتج بدل الانشغال بمحاربة الفوضى. وإذا كنت تبني تطبيقًا سيكبر مع الوقت، ففهم Store وActions وReducers ليس مجرد معرفة نظرية، بل خطوة مهمة نحو كتابة كود أكثر نضجًا، وأكثر ثقة، وأكثر قابلية للحياة.

#Store #Actions #Reducers #React #JavaScript #إدارة الحالة #شرح Redux بالعربية #Redux Toolkit #State Management #تعلم Redux #Redux للمبتدئين

اشترك في نشرتنا البريدية

12k+

المشتركون

أسبوعيًا

التكرار

مجاني

دائمًا