State Reducer Pattern in React

رياكت تتميز بمرونتها الفكرية، حيث تُقدم للمطورين أدوات متعددة مع ترك الحرية لهم في اختيار كيفية بناء مشاريعهم. هاته الخاصية تجعل من ريكات مكتبة قوية ومرنة، مما تسمح بابتكار حلول مخصصة
State Reducer Pattern in React

في هذه الصفحة

رياكت تتميز بمرونتها الفكرية "loosely opinionated"، حيث تُقدم للمطورين أدوات متعددة مع ترك الحرية لهم في اختيار كيفية بناء مشاريعهم. هاته الخاصية تجعل من ريكات مكتبة قوية ومرنة، مما تسمح بابتكار حلول مخصصة تلبي احتياجات مشاريع محددة. ولكن تأتي هاته الحرية بتحديات أيضا، خصوصا عندما يتعلق الأمر بالاتساق (consistency) والصيانة (maintainability) في كود المشروع.

 لمواجهة هذه التحديات، يبرز دور أنماط التصميم (Design Patterns) كطُرق قيمة لضمان تطبيق الممارسات الجيدة والحفاظ على جودة الكود. من بين هذه الأنماط لدينا State Reducer Pattern الذي يُعد من الأنماط الفعالة التي تساعد في إدارة تحديثات حالة التطبيق (state) بشكل أكثر تحكما و سلاسة. بفضل Kent C .Dodds الذي قام بتقديمه لمُجتمع React، يقدم هذا النمط طريقة منظمة للتعامل مع التحديثات على الحالة (state updates) باستخدام useReducer، مما يعزز من قابلية الصيانة والفعالية في مشاريع React. دعونا نستكشف معا كيف يمكن أن يُحسن هذا النمط من جودة تطبيقاتنا. 


تعريف نمط The State Reducer

هو نمط تصميم في برمجة واجهات المستخدم يُمكن المطورين من التحكم بشكل أكبر في كيفية تحديث الحالة داخل مكونات React. يتم ذلك من خلال استخدام دالة (Reducer Function) تدير التحديثات الحالية بناءً على إجراءات مُعرفة (Actions)، مما يسمح بالتحكم الداخلي والخارجي في سلوك المكون. هذا النمط يُعزز من مرونة المكونات وقابليتها للتخصيص، ويُسهل تطبيق مبدأ "التحكم بالعكس" (Inversion of Control) لإتاحة الفرصة للمستخدمين بتعديل سلوك التطبيق دون تغيير التنفيذ الداخلي للمكون.


التطبيق العملي للنمط

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

سنقوم بإنشاء مُكون Dropdown عند الضغط عليه سيُظهر لنا مجموعة من العناصر و التي يُمكننا الاختيار منها.

import { useState } from 'react'

// Without State Reducer Pattern
const DropdownWithoutStateReducerPattern = ({ items }) => {
  const [isOpen, setIsOpen] = useState(false);
  const [selectedItem, setSelectedItem] = useState(null);

  function toggleDropdown() {
    setIsOpen(!isOpen);
  }

  function handleSelect(item) {
    setSelectedItem(item);
    setIsOpen(false); // Default behavior: close on selection
  }

  return (
    <div>
      <button type="button" onClick={toggleDropdown}>
        Select an Item
      </button>
      {isOpen && (
        <ul>
          {items.map((item) => (
            <li key={item} onClick={() => handleSelect(item)}>
              {item}
            </li>
          ))}
        </ul>
      )}
      {selectedItem && <p>Selected Item: {selectedItem}</p>}
    </div>
  );
}

const App = () => {
  return (
    <DropdownWithoutStateReducerPattern
      items={["Item 1", "Item 2", "Item 3"]}
    />
  );
}

export default App

سلوك المُكون في الوقت الحالي عند اختيار أحد العناصر هو غلق الـ Dropdown. الآن ماذا لو يُريد المُستخدم أن يُغير في هذا السلوك؟ هنا ظهر لنا مُتطلب جديد، فبدلا من غلق المُكون عند اختيار عُنصر ما فنحن نُريد تركه مفتوحا.

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


import { useState } from 'react'

// Without State Reducer Pattern
const DropdownWithoutStateReducerPattern = ({ items, keepOpenOnSelect }) => {
  const [isOpen, setIsOpen] = useState(false);
  const [selectedItem, setSelectedItem] = useState(null);

  function toggleDropdown() {
    setIsOpen(!isOpen);
  }

  function handleSelect(item) {
    setSelectedItem(item);
    if (!keepOpenOnSelect) {
      setIsOpen(false);
    }
  }

  return (
    <div>
      <button type="button" onClick={toggleDropdown}>
        Select an Item
      </button>
      {isOpen && (
        <ul>
          {items.map((item) => (
            <li key={item} onClick={() => handleSelect(item)}>
              {item}
            </li>
          ))}
        </ul>
      )}
      {selectedItem && <p>Selected Item: {selectedItem}</p>}
    </div>
  );
}

const App = () => {
  return (
    <>
	    <DropdownWithoutStateReducerPattern
	      items={["Item 1", "Item 2", "Item 3"]}
	    />
	    <DropdownWithoutStateReducerPattern
		    keepOpenOnSelect={true}
	      items={["Item 1", "Item 2", "Item 3"]}
	    />
    </>
  );
}

export default App

العيوب:

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

💡
نمط State Reducer يُقدم حلاً أكثر كفاءة ومرونة لهذه التحديات، من خلال إعطاء المستخدم القدرة على تحديد سلوك المكون من خارجه، دون الحاجة للتدخل المباشر في تنفيذه أو الاعتماد على props مُحددة.

خُطوات الحل:

  • تعريف الحالة الأولية initial state والإجراءات actions: نبدأ بتحديد الحالة الأولية لمكون Dropdown و الإجراءات التي يُمكن أن يتخذها المستخدم.
  • كتابة الـ reducer: نكتب دالة reducer التي تتعامل مع الإجراءات وتُحدث الحالة بناءً على نوع الإجراء المُرسل.
  • تطبيق useReducer في مكون Dropdown: نستخدم useReducer لإدارة الحالة داخل المكون بدلا من useState، مما يُسهل التحكم في سلوكه وتوسيع قابلياته.
  • توفير واجهة برمجية للتخصيص: نُتيح للمستخدمين تمرير reducer خاص بهم إلى المكون لتخصيص سلوكه بشكل أكثر دقة.

import { useReducer } from 'react'

// With State Reducer Pattern

// Action Types
const actions = {
  TOGGLE: 'TOGGLE',
  SELECT: 'SELECT'
};

// Define reducer
const dropdownReducer = (state, action) => {
  switch (action.type) {
    case actions.TOGGLE:
      return { ...state, isOpen: !state.isOpen };
    case actions.SELECT:
      return { ...state, selectedItem: action.payload, isOpen: false }; // Default behavior
    default:
      return state;
  }
};

const DropdownWithStateReducerPattern = ({
  items,
  reducer = dropdownReducer,
}) => {
  const [state, dispatch] = useReducer(reducer, {
    isOpen: false,
    selectedItem: null
  });

  const toggleDropdown = () => dispatch({ type: actions.TOGGLE });

  const handleSelect = (item) => dispatch({ type: actions.SELECT, payload: item });

  return (
    <div>
      <button type="button" onClick={toggleDropdown}>
        Select an Item
      </button>
      {state.isOpen && (
        <ul>
          {items.map((item) => (
            <li key={item} onClick={() => handleSelect(item)}>
              {item}
            </li>
          ))}
        </ul>
      )}
      {state.selectedItem && <p>Selected Item: {state.selectedItem}</p>}
    </div>
  );
};

function customReducer(state, action) {
  switch (action.type) {
    case actions.SELECT:
      // Keep dropdown open upon selection
      return { ...state, selectedItem: action.payload, isOpen: true };
    default:
      return dropdownReducer(state, action); // Fallback to default behavior for other actions
  }
}

const App = () => {
  return (
	  <>
		  <DropdownWithStateReducerPattern
        items={["Item 1", "Item 2", "Item 3"]}
      />
      <DropdownWithStateReducerPattern
        items={["Item 1", "Item 2", "Item 3"]}
        reducer={customReducer}
      />
	  </>
  );
}

export default App

في ختام رحلتنا مع نمط State Reducer، يظهر بوضوح كيف يمكن لهذا النمط أن يُعزز من قوة ومرونة مكونات React. من خلال توفير آلية لـ"التحكم بالعكس - inversion of control"، يُمكن للمطورين التحكم في سلوك المكونات من خارجها، مما يسمح بتخصيص دقيق وفعال دون الحاجة إلى تغيير تنفيذها الداخلي أو التأثير على الواجهة البرمجية للمُكونات. 

اشترك الآن بنشرة اقرأ-تِك الإخبارية

لا تدع أي شيء يفوتك. واحصل على أحدث المقالات المميزة مباشرة إلى بريدك الإلكتروني وبشكل مجاني!
رائع! تحقق من بريدك الإلكتروني واضغط على الرابط لإكمال تسجيل الدخول.
حدث خطأ! من فضلك قم بادخال بريد إلكتروني صحيح!