اذهب إلى المحتوى

إنشاء خطافات مخصصة (Custom Hooks) في تطبيقات React


ابراهيم الخضور

ستختلف التمارين في هذا القسم عن تمارين القسم السابق. فستجد في هذا الفصل وفي الفصل الذي سبقه تمارين تتعلق بالأفكار التي سنقدمها في هذا الفصل، بالإضافة إلى سلسلة من التمارين التي سنراجع من خلالها ما تعلمناه خلال تقدمنا في مادة المنهاج، وذلك بتعديل التطبيق Bloglist الذي عملنا عليه في القسم 4 والقسم 5 ومراجعته وتطبيق المهارات التي تعلمناها.

الخطافات

تقدم المكتبة React 10 أنواع من الخطافات المدمجة، من أكثرها شهرة useState وuseEffect والتي استخدمناها كثيرًا. كما استخدمنا في القسم 5 الخطاف useImperativeHandle والذي يسمح للمكوّنات بتقديم الدوال الخاصة بها إلى مكوّنات أخرى.

بدأت مكتبات React خلال السنتين الماضيتين بتقديم واجهات برمجية مبنية على الخطافات. فقد استخدمنا في القسم السابق الخطافين useSelector وuseDispatch من المكتبة React-Redux لمشاركة مخزن Redux وإيفاد الدوال إلى المكوّنات. فواجهة Redux البرمجية المبنية على الخطافات أسهل استعمالًا من سابقتها التي لازالت قيد الاستعمال connect.

تعتبر الواجهة البرمجية React-router التي قدمناها في الفصل السابق مبنية جزئيًا على أساس الخطافات. حيث تُستعمل خطافاتها للولوج إلى معاملات العناوين وكائن المحفوظات، وتعديل العنوان على المتصفح برمجيًا.

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

لا تستدعي الخطافات من داخل الحلقات أو العبارات الشرطية أو الدوال المتداخلة، بل استخدمها دائمًا عند أعلى مستويات دالة React.

لا تستدعي الخطافات من داخل دول JavaScript النظامية، بل يمكنك أن:

  • استدعاء الخطافات من مكوّنات دوال React

  • استدعاء الخطافات من قبل خطافات مخصصة.

هنالك قاعدة يقدمها المدقق ESlint للتأكد من استدعاء الخطافات بالشكل الصحيح، حيث تُهيئ التطبيقات المبنية بواسطة create-react-app القاعدة eslint-plugin-react-hooks بشكل دائم، لكي تنبهك عند استعمال الخطاف بطريقة غير مشروعة:

eslint_hooks_use_error_01.png

خطافات مخصصة

تقدم لك Recat ميزة إنشاء خطافات خاصة بك. وتعتبر React أن الغرض الرئيسي من هذه الخطافات هو تسهيل إعادة استخدام شيفرة مكوّن في مكان آخر.

اقتباس

يمكّنك بناء خطافاتك الخاصة من نقل منطق المكوّن إلى دوال قابلة للاستخدام مرة أخرى.

إنّ الخطافات المخصصة هي دوال JavaScript نظامية يمكنها استعمال أية خطافات أخرى طالما أنها تخضع لقواعد استخدام الخطافات. بالإضافة إلى ضرورة أن يبدأ اسم الخطاف بالكلمة "use".

لقد أنشأنا في القسم 1 تطبيق عداد يمكن زيادة قيمته أو إنقاصها أو تصفيره. وكانت شيفرته كالتالي:

import React, { useState } from 'react'
const App = (props) => {
  const [counter, setCounter] = useState(0)

  return (
    <div>
      <div>{counter}</div>
      <button onClick={() => setCounter(counter + 1)}>
        plus
      </button>
      <button onClick={() => setCounter(counter - 1)}>
        minus
      </button>      
      <button onClick={() => setCounter(0)}>
        zero
      </button>
    </div>
  )
}

لننقل منطق العداد ونضعه ضمن خطاف مخصص. ستبدو شيفرة الخطاف كالتالي:

const useCounter = () => {
  const [value, setValue] = useState(0)

  const increase = () => {
    setValue(value + 1)
  }

  const decrease = () => {
    setValue(value - 1)
  }

  const zero = () => {
    setValue(0)
  }

  return {
    value, 
    increase,
    decrease,
    zero
  }
}

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

يمكن أن يستخدم تطبيق React هذا الخطاف كالتالي:

const App = (props) => {
  const counter = useCounter()

  return (
    <div>
      <div>{counter.value}</div>
      <button onClick={counter.increase}>
        plus
      </button>
      <button onClick={counter.decrease}>
        minus
      </button>      
      <button onClick={counter.zero}>
        zero
      </button>
    </div>
  )
}

وهكذا يمكننا نقل حالة المكون App ومنطق التعامل معه إلى الخطاف useCounterhook. وسيتولى هذا الخطاف التعامل مع حالة ومنطق التطبيق. يمكن إعادة استخدام هذا الخطاف في التطبيق الذي يراقب عدد نقرات الزر الأيمن والأيسر للفأرة:

const App = () => {
  const left = useCounter()
  const right = useCounter()

  return (
    <div>
      {left.value}
      <button onClick={left.increase}>
        left
      </button>
      <button onClick={right.increase}>
        right
      </button>
      {right.value}
    </div>
  )
}

يُنشئ التطبيق عدادين منفصلين تمامًا. يُسند الأول إلى المتغيّر left ويُسند الثاني إلى المتغير right.

يصعب التعامل أحيانًا مع نماذج React. يستخدم التطبيق التالي نموذجًا يطلب من المستخدم أن يُدخل اسمه وتاريخ ميلاده وطوله:

const App = () => {
  const [name, setName] = useState('')
  const [born, setBorn] = useState('')
  const [height, setHeight] = useState('')

  return (
    <div>
      <form>
        name: 
        <input
          type='text'
          value={name}
          onChange={(event) => setName(event.target.value)} 
        /> 
        <br/> 
        birthdate:
        <input
          type='date'
          value={born}
          onChange={(event) => setBorn(event.target.value)}
        />
        <br /> 
        height:
        <input
          type='number'
          value={height}
          onChange={(event) => setHeight(event.target.value)}
        />
      </form>
      <div>
        {name} {born} {height} 
      </div>
    </div>
  )
}

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

لنعرّف الخطاف المخصص useFieldhook الذي سيسهل إدارة حالة النموذج:

const useField = (type) => {
  const [value, setValue] = useState('')

  const onChange = (event) => {
    setValue(event.target.value)
  }

  return {
    type,
    value,
    onChange
  }
}

تتلقى دالة الخطاف نوع حقل الدخل (مربع النص) كمعامل. وتعيد الدالة كل الصفات التي يتطلبها عنصر الدخل، وهي نوعه وقيمته ومعالج الحدث onChange.

يمكن استخدام الخطاف بالطريقة التالية:

const App = () => {
  const name = useField('text')
  // ...

  return (
    <div>
      <form>
        <input
          type={name.type}
          value={name.value}
          onChange={name.onChange} 
        /> 
        // ...
      </form>
    </div>
  )
}

نشر الصفات

يمكننا تسهيل الأمور أكثر. يحتوي الكائن name على كل الصفات التي يحتاجها عنصر الدخل من خصائص هذا الكائن، وبالتالي يمكننا تمرير هذه الخصائص إلى العنصر باستخدام عبارة النشر كالتالي:

<input {...name} /> 

وكما ستجد في المثال الموجود ضمن توثيق React، سيعطي استخدام أي من الطريقتين التاليتين في تمرير الخصائص النتيجة نفسها:

<Greeting firstName='Arto' lastName='Hellas' />

const person = {
  firstName: 'Arto',
  lastName: 'Hellas'
}

<Greeting {...person} />

يمكن تبسيط التطبيق إلى الشكل التالي:

const App = () => {
  const name = useField('text')
  const born = useField('date')
  const height = useField('number')

  return (
    <div>
      <form>
        name: 
        <input  {...name} /> 
        <br/> 
        birthdate:
        <input {...born} />
        <br /> 
        height:
        <input {...height} />
      </form>
      <div>
        {name.value} {born.value} {height.value}
      </div>
    </div>
  )
}

سيسهل استخدام النماذج عندما نستخدم الخطافات المخصصة في تغليف بعض التفاصيل المزعجة المتعلقة بمزامنة الحالة.

ومن الواضح أنّ الخطافات المخصصة ليست فقط أداة لإعادة استخدام الشيفرة، بل تقدم طرق أفضل في تقسيم شيفرتنا إلى وحدات أصغر.

المزيد حول الخطافات

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

التمارين 7.4 - 7.8

سنستمر في العمل على التطبيق الذي ورد في تمارين الفصل السابق React-Router من هذا القسم.

7.4 تطبيق الطرائف باستعمال الخطافات: الخطوة 1

بسط عملية إنشاء طرفة جديدة من خلال النموذج في تطبيقك باستعمال الخطاف المخصص useFieldhook الذي عرفناه سابقًا.

المكان الطبيعي لحفظ الخطاف المخصص في تطبيقك قد يكون الملف "src/hooks/index.js/".

وانتبه عند استخدام التصدير المحدد بدلًا من الافتراضي:

import { useState } from 'react'

export const useField = (type) => {
  const [value, setValue] = useState('')

  const onChange = (event) => {
    setValue(event.target.value)
  }

  return {
    type,
    value,
    onChange
  }
}

// modules can have several named exports
export const useAnotherHook = () => {  
  // ...
}

فستتغير طريقة إدراج الخطاف إلى الشكل:

import  { useField } from './hooks'

const App = () => {
  // ...
  const username = useField('text')
  // ...
}

7.5 تطبيق الطرائف باستعمال الخطافات: الخطوة 2

أضف زرًا إلى النموذج بحيث يمكنك مسح كل المعلومات في عناصر الإدخال:

anecdotes_clearr_all_02.png

وسع وظيفة الخطاف useFieldhook لكي يقدم طريقة لمسح كل بيانات حقول النموذج.

وتبعًا لحلك المقترح قد تجد نفسك أمام التحذير التالي على الطرفية:

custom_hook_warn_03.png

سنعود إلى هذا التحذير في التمرين التالي.

7.6 تطبيق الطرائف باستعمال الخطافات: الخطوة 3

إن لم يظهر التحذير السابق في حلك فقد أنجزت بالفعل حل هذا التمرين.

إن ظهر معك ذلك التحذير فعليك إجراء التعديلات المناسبة للتخلص من القيمة غير المناسبة للخاصية reset ضمن المعرّف <input>.

إنّ سبب هذا التحذير هو أن تعديل التطبيق ليستعمل نشر الصفات كالتالي:

<input {...content}/>

مطابق تمامًا للشيفرة التالية:

<input
  value={content.value} 
  type={content.type}
  onChange={content.onChange}
  reset={content.reset}
/>

لكن لا يجب أن نسند الصفة reset إلى عنصر الإدخال input. إنّ أحد الحلول المقترحة هو عدم استعمال صيغة النشر وكتابة الصفات كالتالي:

<input
  value={username.value} 
  type={username.type}
  onChange={username.onChange}
/>

إن كنت ستستخدم هذا الحل فستخسر العديد من الميزات التي يقدمها الخطاف useFieldhook. فكّر بدلًا من ذلك بحل يستخدم طريقة النشر.

7.7 خطاف لتطبيق الدول:

لنعد إلى التمارين 12 إلى 14 من القسم 2.

استعمل الشيفرة الموجودة على GitHub كقاعدة انطلاق.

يُستخدم التطبيق للبحث عن تفاصيل دولة محددة بالاستعانة بالواجهة https://restcountries.eu. فإن عُثر على الدولة ستُعرض معلوماتها على الشاشة.

country_hook_04.png

وإن لم يُعثر على الدولة ستظهر رسالة تبلغ فيها المستخدم بذلك.

country_not_founr_05.png

إنّ التطبيق السابق مكتمل وجاهز للعمل، لكن عليك في هذه التمرين إنشاء خطاف مخصص باسم useCountry يستخدم للبحث عن تفاصيل الدولة التي تُمرر إلى الخطاف كمعامل.

استخدم الوصلة full name من الواجهة البرمجية السابقة لإحضار بيانات الدولة مستخدمًا الخطاف useEffect ضمن خطافك المخصص. ولاحظ أنه من الضروري استخدام مصفوفة المعامل الثاني للخطاف useEffect في هذا التمرين للتحكم بتوقيت تنفيذ دالة المؤثر.

7.8 الخطافات الكاملة

ستبدو الشيفرة المسؤولة عن الاتصال مع الواجهة الخلفية لتطبيق الملاحظات من القسم السابق على النحو التالي:

import axios from 'axios'
const baseUrl = '/api/notes'

let token = null

const setToken = newToken => {
  token = `bearer ${newToken}`
}

const getAll = () => {
  const request = axios.get(baseUrl)
  return request.then(response => response.data)
}

const create = async newObject => {
  const config = {
    headers: { Authorization: token },
  }

  const response = await axios.post(baseUrl, newObject, config)
  return response.data
}

const update = (id, newObject) => {
  const request = axios.put(`${ baseUrl } /${id}`, newObject)
  return request.then(response => response.data)
}

export default { getAll, create, update, setToken }

لا توحي الشيفرة السابقة أبدًا بأنّ تطبيقنا هو للتعامل مع الملاحظات. وباستثناء قيمة المتغير baseUrl يمكن استخدام الشيفرة السابقة مع تطبيق قائمة المدونات للاتصال مع الواجهة الخلفية.

افصل شيفرة الاتصال مع الواجهة الخلفية ضمن خطاف مخصص لها باسمuseResource. يكفي إحضار كل الموارد بالإضافة إلى إنشاء مورد جديد.

يمكنك تنفيذ التمرين من أجل المشروع الموجود في المستودع https://github.com/fullstack-hy2020/ultimate-hooks. للمكوّن App لهذا المشروع الشيفرة التالية:

const App = () => {
  const content = useField('text')
  const name = useField('text')
  const number = useField('text')

  const [notes, noteService] = useResource('http://localhost:3005/notes')
  const [persons, personService] = useResource('http://localhost:3005/persons')

  const handleNoteSubmit = (event) => {
    event.preventDefault()
    noteService.create({ content: content.value })
  }

  const handlePersonSubmit = (event) => {
    event.preventDefault()
    personService.create({ name: name.value, number: number.value})
  }

  return (
    <div>
      <h2>notes</h2>
      <form onSubmit={handleNoteSubmit}>
        <input {...content} />
        <button>create</button>
      </form>
      {notes.map(n => <p key={n.id}>{n.content}</p>)}

      <h2>persons</h2>
      <form onSubmit={handlePersonSubmit}>
        name <input {...name} /> <br/>
        number <input {...number} />
        <button>create</button>
      </form>
      {persons.map(n => <p key={n.id}>{n.name} {n.number}</p>)}
    </div>
  )
}

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

إن أضفت الخطاف بشكله الصحيح يمكنك إعادة استخدامه في تطبيق الملاحظات ودليل الهاتف (شغل الخادم باستعمال الأمر npm run server على المنفذ 3005).

reuse_custom_hook_06.png

ترجمة -وبتصرف- للفصل Custom hooks من سلسلة Deep Dive Into Modern Web Development


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...