03.01.2024 14 min read

أنماط React المتقدمة

بواسطة Mirza Leka

نظرة معمّقة على أنماط React المتقدمة بما فيها Compound Components وRender Props وCustom Hooks وHOCs لبناء مكونات واجهة مستخدم مرنة وقابلة لإعادة الاستخدام.

React components illustration

تعرّف على بعض أنماط React المتقدمة وكيفية استخدامها.

ما هو React؟

React مكتبة JavaScript مفتوحة المصدر مصممة لبناء واجهات المستخدم، خاصة لتطبيقات الصفحة الواحدة حيث تكون التحديثات السلسة والفعّالة أمرًا بالغ الأهمية. طوّرتها وتحافظ عليها Facebook، وتستخدم React صياغة تعريفية (declarative) تتيح للمطورين وصف حالة الواجهة المطلوبة، وتقوم تلقائيًا بتحديث واجهة المستخدم عند تغيّر البيانات الأساسية.

ما تقدّمه React:

  • تنظّم واجهة المستخدم في مكونات قابلة لإعادة الاستخدام.
  • تبسّط عملية بناء الواجهة وتحديثها.
  • تعزّز الأداء من خلال تقليل التلاعب بـ DOM.
  • تضمن تدفقًا أحادي الاتجاه للبيانات لسلوك يمكن التنبؤ به.

مكونات React

مكونات React هي اللبنات الأساسية لأي تطبيق React، إذ تحتوي على جزء مستقل من واجهة المستخدم ووظيفته المرتبطة به. يمكن اعتبار هذه المكونات بمثابة عناصر HTML مخصصة وقابلة لإعادة الاستخدام يقوم المطورون بتركيبها لإنشاء واجهات معقدة وتفاعلية. يُصمَّم كل مكوّن React لإدارة حالته الخاصة، ما يتيح بنية كود معيارية (modular) وقابلة للصيانة.

React المتقدم

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

لنلقِ أولًا نظرة على الفوائد التي يمكننا تحقيقها عند استخدام أنماط React التي سنناقشها في هذا المقال:

  1. تنظيم وصيانة أفضل للكود: يؤدي استخدام تقنيات برمجة متقدمة في React إلى تحسين تنظيم الكود. تشجّع هذه الأساليب على المعيارية وإعادة الاستخدام، ما يسهّل إدارة وصيانة قواعد الكود الكبيرة. غالبًا ما تكون المكونات المبنية باستخدام هذه التقنيات أكثر تخصصًا، ما يعزز بنية كود معيارية وقابلة للصيانة. تسهّل هذه البنية المعيارية عملية التصحيح والاختبار والتعاون بين أعضاء الفريق.
  2. زيادة قابلية إعادة الاستخدام والتركيب: تمكّن تقنيات React المتقدمة المطورين من بناء مكونات ومنطق قابل لإعادة الاستخدام. على سبيل المثال، تشجّع بعض الأساليب على تركيب مكونات صغيرة متخصصة في مكونات أكثر تعقيدًا. باتباع هذه الممارسات، يمكن للمطورين إنشاء مكونات تحتوي وظيفة محددة، ما يسهّل إعادة استخدامها عبر أقسام مختلفة من التطبيق. تقلل قابلية إعادة الاستخدام هذه من التكرار وتعزز نهجًا متسقًا وموحدًا لتنفيذ الميزات.
  3. أداء مُحسَّن: تسهم عدة تقنيات برمجة متقدمة في React، إلى جانب ميزات التحسين المدمجة، في تحسين الأداء. تساعد هذه التقنيات في منع إعادة العرض غير الضرورية للمكونات، ما يضمن حدوث التحديثات فقط عند الضرورة. من خلال استخدام استراتيجيات التحسين مثل منع إعادة العرض غير الضرورية، يمكن للمطورين تعزيز أداء العرض. وهذا مهم بشكل خاص للتطبيقات ذات الواجهات المعقدة أو مجموعات البيانات الكبيرة.
  4. إدارة فعالة للحالة والتأثيرات الجانبية: تقدّم تقنيات React المتقدمة، بما فيها الأساليب المخصصة لإدارة الحالة، وسيلة قوية للتعامل مع الحالة والتأثيرات الجانبية. تتيح هذه التقنيات احتواء المنطق المعقد، ما يعزز نهجًا نظيفًا وفعالًا لإدارة الحالة. من خلال الاستفادة من هذه التقنيات، يمكن للمطورين إدارة الحالة بطريقة أكثر قابلية للتنبؤ وقابلة للتوسع، خاصة في السيناريوهات التي يصبح فيها منطق الحالة معقدًا. بالإضافة إلى ذلك، تساعد الأساليب المخصصة في احتواء التأثيرات الجانبية، ما يبسّط فهم الكود واختباره.

Render Props

Render Props هو نمط تصميم في React يتضمن تمرير دالة كـ prop إلى مكوّن، ما يتيح للمكوّن تفويض عرض محتواه إلى الدالة الممرَّرة.

قد يبدو الأمر معقدًا، لكنه ليس كذلك فعليًا... لنرَ ذلك في مثال:

function ToggleOn({on, children}) {
  return on ? children : null
}

function ToggleOff({on, children}) {
  return on ? null : children
}

function ToggleButton({on, toggle, ...props}) {
  return <Switch on={on} onClick={toggle} {...props} />
}

أعلاه لدينا مجموعة من المكونات نعمل عليها. سنستخدم مثال Toggle عبر أنماط متعددة، لذا تأكد من فهمك لما تفعله هذه المكونات.

الآن لنفترض أننا نريد استخدام هذه المكونات في تطبيق React لدينا. الطريقة الساذجة ستكون:

function App() {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(!on)

  return (
    <>
        <ToggleOn on={on}>The button is on</ToggleOn>
        <ToggleOff on={on}>The button is off</ToggleOff>
        <ToggleButton on={on} toggle={toggle} />
        <SomeLargeComponent />
    </>
  )
}

هناك مشكلة واضحة في هذا النهج "الساذج". هل يمكنك تحديدها؟

المشكلة أن الحالة "عامة" بمعنى ما، ومكشوفة لجميع المكونات، بما فيها "SomeLargeComponent". إذا كنت تعرف كيف يعمل React داخليًا، فأنت تعلم أن تغيير حالة "on" سيؤدي إلى إعادة عرض جميع المكونات في App، بما فيها "SomeLargeComponent". نريد تجنّب ذلك.

من الجيد أيضًا أن نفصل المنطق بحيث يكون لدينا مكوّن "عرض" ومكوّن "حاوية" منفصلان.

يمكننا تحقيق ذلك باستخدام render props:

function Toggle({ render }) {
  const [on, setOn] = React.useState(false);
  const toggle = () => setOn(!on);

  return render({ on, toggle });
}

function App() {
  return (
    <Toggle
      render={({ on, toggle }) => (
        <>
          <ToggleOn on={on}>The button is on</ToggleOn>
          <ToggleOff on={on}>The button is off</ToggleOff>
          <ToggleButton on={on} toggle={toggle} />
        </>
      )}
    />
    <SomeLargeComponent />
  );
}

أنشأنا مكوّن Toggle سيكون مسؤولًا بالكامل عن التعامل مع الحالة داخليًا. نزوّد مكوّن Toggle بخاصية render تُدعى "render"، وهي دالة تقبل الحالة (أو أي شيء نريده) الممرَّرة من مكوّن Toggle الذي يحتفظ بالحالة والمنطق.

يمكننا أيضًا استخدام خاصية children كـ render prop. في تلك الحالة سيبدو الكود كالتالي:

function Toggle({ children }) {
  const [on, setOn] = React.useState(false);
  const toggle = () => setOn(!on);

  return children({ on, toggle });
}

function App() {
  return (
    <Toggle
      {({ on, toggle }) => (
        <>
          <ToggleOn on={on}>The button is on</ToggleOn>
          <ToggleOff on={on}>The button is off</ToggleOff>
          <ToggleButton on={on} toggle={toggle} />
        </>
      )}
    />
    <SomeLargeComponent />
  );
}

بهذه الطريقة وفّرنا "SomeLargeComponent" من إعادة العرض، وحللنا مشكلة فصل المنطق.

المشكلات التي حاولنا حلها باستخدام render props استُبدلت إلى حد كبير بـ React Hooks. فبما أن Hooks غيّرت الطريقة التي يمكننا بها إضافة قابلية إعادة الاستخدام ومشاركة البيانات إلى المكونات، يمكنها استبدال نمط render props في كثير من الحالات. إلى جانب ذلك، أردت أيضًا تناول render props لأنه لا يزال مستخدَمًا وربما ستستخدمه، أو استخدمته من قبل.

Compound Components

Compound Components في React هو نمط تصميم تعمل فيه مجموعة من المكونات معًا لتشكيل وحدة متماسكة، ما يتيح تركيب بنى واجهة مستخدم معقدة مع الحفاظ على واجهة برمجية (API) واضحة وبديهية. على عكس المكونات النموذجية التي تعمل بشكل مستقل، صُممت المكونات المركّبة لتُستخدَم معًا كمجموعة لتحقيق وظيفة أو مظهر محدد. يعزز هذا النمط تنظيم الكود، ويشجّع على إعادة الاستخدام، ويوفّر واجهة أكثر تعريفية وبداهة للمطورين.

سيكون الأمر أوضح بمثال، لنتذكّر أولًا كيف يبدو الحل "الساذج" لمشكلة toggle:

function App() {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(!on)

  return (
    <>
        <ToggleOn on={on}>The button is on</ToggleOn>
        <ToggleOff on={on}>The button is off</ToggleOff>
        <ToggleButton on={on} toggle={toggle} />
        <SomeLargeComponent/>
    </>
  )
}

لنرَ كيف سيبدو حل Compound Components:

function Toggle({children}) {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(!on)

  return React.Children.map(children, (child, index) => {
    return React.cloneElement(child, {
      on,
      toggle,
    })
  })
}

function App() {
  return (
    <>
      <Toggle>
        <ToggleOn>The button is on</ToggleOn>
        <ToggleOff>The button is off</ToggleOff>
        <ToggleButton />
      </Toggle>
      <SomeLargeComponent/>
    </>
  )
}

يمكننا أن نرى أن لدينا شيئًا مشابهًا لحل render props. الفرق الرئيسي في مكوّن App هو أننا لم نعد نمرر الخصائص عبر طبقات متعددة (prop drilling) - (مع العلم أننا لا نزال نستخدم نفس مجموعة مكونات toggle) - وهذا رائع. تُشارَك الحالة ضمنيًا داخل مكوّن Toggle الجديد المعرَّف.

الجزء الأساسي في هذا النمط هو:

React.Children.map(children, (child, index) => {
  return React.cloneElement(child, {
    on,
    toggle,
  })
})

هنا نأخذ الأبناء (children) المزوَّدة للمكوّن، ونمر عبر كل ابن، ونستنسخه، وأخيرًا المعامل الأخير المزوَّد لدالة "cloneElement" يمرر يدويًا "props" إلى عنصر React الجديد. بهذه الطريقة يحصل جميع الأبناء على الخصائص المطلوبة، والمزوَّدة من مكوّن Toggle الذي يحتوي مرة أخرى على كل المنطق والحالة بداخله.

يبدو هذا رائعًا، لكن له عيوبه. على سبيل المثال، إذا أردنا شيئًا كهذا:

function App() {

  return (
    <>
      <Toggle>
        <ToggleOn>The button is on</ToggleOn>
        <ToggleOff>The button is off</ToggleOff>
        <span>Hello</span>
        <ToggleButton />
      </>
      <SomeLargeComponent/>
    </>
  )
}

سنحصل على خطأ يشير إلى أنه لا يمكننا استنساخ عنصر "span". الحل؟

function Toggle({children}) {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(!on)

  return React.Children.map(children, (child, index) => {
    if (typeof child.type === 'string') return child
    return React.cloneElement(child, {
      on,
      toggle,
    })
  })
}

إضافة شرط بسيط يحل هذه المشكلة، لأن نوع المكوّن المُنشَأ يدويًا هو دائمًا كائن (object). من خلال التحقق مما إذا كان النوع سلسلة نصية (string)، يمكننا فقط إعادة العنصر كما هو دون استنساخه.

لكن ماذا لو أردنا شيئًا كهذا:

function App() {

  return (
  <>
    <Toggle>
      <ToggleOn>The button is on</ToggleOn>
      <ToggleOff>The button is off</ToggleOff>
      <div>
        <span>Hello</span>
      </div>
      <ToggleButton />
    </>
    <SomeLargeComponent />
  </>
  )
}

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

Flexible Compound Components

هذا في الأساس نفس نمط compound components، والفرق الرئيسي هو أن حالة الأب لا تُمرَّر مباشرة إلى المكونات الأبناء، بل عبر context. يوفّر هذا نهجًا أكثر ديناميكية وقابلية للتوسع لإدارة الحالة المشتركة ضمن تسلسل هرمي للمكونات.

هناك قدر لا بأس به من إعادة الهيكلة (refactoring) المطلوبة. لنبدأ بإنشاء الـ context الخاص بنا:

const ToggleContext = React.createContext()

الآن لننشئ hook لاستخدام الـ context الجديد لدينا:

function useToggle() {
  return React.useContext(ToggleContext)
}

في مكوّن Toggle لدينا، نحتاج إلى تغليف الأبناء بمزوّد context. نمرر الحالة المحلية إلى المزوّد. عند القيام بذلك، بعبارة بسيطة، تُخزَّن قيم الحالة المزوَّدة في نسخة الـ context. هذا يتيح للأبناء داخل مكوّن Toggle الوصول إلى القيم التي تم توفيرها.

function Toggle({children}) {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(!on)

  return (
    <ToggleContext.Provider value={{on, toggle}}>
      {children}
    </ToggleContext.Provider>
  )
}

الآن نحتاج إلى تغيير مكونات toggle لدينا. لن تعد تقبل الحالة المطلوبة عبر props، بل ستستخدم الـ hook useToggle الذي أنشأناه للوصول إلى الحالة اللازمة.

function ToggleOn({children}) {
  const {on} = useToggle()
  return on ? children : null
}

function ToggleOff({children}) {
  const {on} = useToggle()
  return on ? null : children
}

function ToggleButton({...props}) {
  const {on, toggle} = useToggle()
  return <Switch on={on} onClick={toggle} {...props} />
}

وأخيرًا يمكننا امتلاك أي عدد نريده من عناصر التداخل، وسيعمل هذا الكود كما هو متوقع.

function App() {

  return (
    <>
      <Toggle>
        <ToggleOn>The button is on</ToggleOn>
        <ToggleOff>The button is off</ToggleOff>
        <div>
          <span>Hello</span>
        </div>
        <ToggleButton />
      </>
      <SomeLargeComponent/>
    </>
  )
}

HOCs (المكونات عالية الترتيب)

يتيح هذا النمط تركيب المكونات وإعادة استخدام الكود من خلال تغليف مكوّن واحد أو أكثر بدالة. تأخذ هذه الدالة مكوّنًا كمدخل وتعيد مكوّنًا جديدًا بخصائص أو حالة أو سلوك إضافي. غالبًا ما تُستخدَم HOCs للاهتمامات الشاملة (cross-cutting concerns)، مثل المصادقة أو التسجيل أو جلب البيانات، ما يتيح لنا احتواء الوظيفة ومشاركتها عبر أجزاء مختلفة من التطبيق.

لنفترض أننا أردنا دائمًا إضافة تنسيق معين إلى مكونات متعددة في تطبيقنا. بدلًا من إنشاء كائن تنسيق محليًا في كل مرة، يمكننا ببساطة إنشاء HOC يضيف كائنات التنسيق إلى المكوّن الذي نمرره إليه:

function withStyles(Component) {
  return props => {
    const style = { padding: '0.2rem', margin: '1rem' }
    return <Component style={style} {...props} />
  }
}

const Button = () => <button>Click me!</button>
const Text = () => <p>Hello World!</p>

const StyledButton = withStyles(Button)
const StyledText = withStyles(Text)

كما نرى، أنشأنا دالة withStyles، وهي في الواقع HOC. الكلمة المفتاحية "with" تُستخدَم دائمًا لـ HOCs. يقبل HOC الاسم withStyles مكوّنًا ويعيد مكوّنًا مجهولًا (لا يملك اسمًا). هذا يتيح لنا الحصول على تأثيرات جانبية وخصائص وسلوكيات مختلفة على المكوّن الذي غُلِّف بـ HOC.

لنرَ مثالًا "أكبر"، فقط لأخذ فكرة عمّا يمكن استخدام نمط HOC هذا من أجله:

function withLoader(Element, url) {
  return (props) => {
    const [data, setData] = useState(null);
    
    useEffect(() => {
      async function getData() {
        const res = await fetch(url);
        const data = await res.json();
        setData(data);
      }

      getData();
    }, []);
    
    if (!data) {
      return <div>Loading...</div>;
    }
    
    return <Element {...props} data={data} />;
  };
}

كما نرى، لدينا HOC باسم withLoader. يمكن تسمية هذا الـ HOC withFetch أيضًا، لكن هذا ليس بيت القصيد. يقبل الـ HOC معاملين: المكوّن ورابط URL. داخل الـ HOC نعيد مكوّنًا نجلب فيه البيانات باستخدام الرابط المزوَّد. نتعامل أيضًا مع حالة التحميل بداخله، وأخيرًا نعيد المكوّن المزوَّد مع الخصائص والبيانات المجلوبة حديثًا.

استخدامه بسيط جدًا، كما يمكننا رؤيته في المقتطف التالي:

function DogImages(props) {
  return props.data.message.map((dog, index) => (
    <img src={dog} alt="Dog" key={index} />
  ));
}

export default withLoader(
  DogImages,
  "https://dog.ceo/api/breed/labrador/images/random/6"
);

يمكننا أن نرى في المثال أعلاه أنه داخل مكوّن DogImages المغلَّف، لدينا البيانات المجلوبة بحيث يمكننا سرد صور الكلاب.

هناك أنماط React أخرى موجودة مثل: Controlled Components وContainer and Presentational Components وState Reducer Pattern وControl Props Pattern وغيرها الكثير. لكل نمط حالات استخدامه الخاصة. لا يوجد نمط مثالي. الأنماط التي تناولتها بتفصيل أكبر هنا هي فقط بعض الأنماط التي وجدتها شخصيًا الأكثر شيوعًا في مسيرتي المهنية، ومن الجيد دائمًا معرفة خياراتك وحدودك عند كتابة الكود.

قراءات إضافية عرض الكل
الخطوة التالية

طبّق هذه الأفكار على منتجك

يساعد استوديو فالنس الفرق ذات المخاطر العالية على تحويل الوضوح الهندسي إلى تسليم منتج معياري.