full_stack_101 حالة المكونات Component state ومعالجات الأحدات event handlers في React


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

لنعد إلى العمل مع React، ولنبدأ بمثالٍ جديد:

const Hello = (props) => {
  return (
    <div>
      <p>
        Hello {props.name}, you are {props.age} years old
      </p>
    </div>
  )
}

const App = () => {
  const name = 'Peter'
  const age = 10

  return (
    <div>
      <h1>Greetings</h1>
      <Hello name="Maya" age={26 + 10} />
      <Hello name={name} age={age} />
    </div>
  )
}

الدوال المساعدة لمكون

لنوسع المكوّن Hello بحيث يحدد العام الذي ولد فيه الشخص الذي نحيّيه:

const Hello = (props) => {
  const bornYear = () => {    
    const yearNow = new Date().getFullYear()    
    return yearNow - props.age
  }
  return (
    <div>
      <p>
        Hello {props.name}, you are {props.age} years old
      </p>
      <p>So you were probably born in {bornYear()}</p>
    </div>
  )
}

وُضعت الشيفرة التي تحدد عام الولادة ضمن دالة منفصلة تُستدعى حين يُصيَّر المكوّن. لا داعي لتمرير عمر الشخص إلى الدالة كمُعامل لأنها قادرة على الوصول إلى الخصائص (props) مباشرة. لو تفحّصنا الشيفرة المكتوبة عن قرب، لوجدنا أن الدالة المساعدة (helper function) قد عُرّفت داخل دالة أخرى تحدد عمل المكوّن. لا يُستخدَم هذا الأسلوب في Java كونه أمرًا معقدًا ومزعجًا، لكنه مستخدم جدًا في JavaScript.

التفكيك Destructuring

قبل أن نمضي قدمًا، سنلقي نظرة على ميزة بسيطة لكنها مفيدة أضيفت في النسخة ES6. تسمح هذه الميزة بتفكيك القيم عن الكائنات والمصفوفات عند إسنادها. لقد ربطنا البيانات التي مررناها إلى المكوّن مستخدمين العبارتين props.name وprops.age. كما كررنا أيضا العبارة props.age مرتين في الشيفرة. وطالما أن props هو كائن له الشكل التالي:

props = {
  name: 'Arto Hellas',
  age: 35,
}

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

const Hello = (props) => {
  const name = props.name  
  const age = props.age
  const bornYear = () => new Date().getFullYear() - age

  return (
    <div>
      <p>Hello {name}, you are {age} years old</p>
      <p>So you were probably born in {bornYear()}</p>
    </div>
  )
}

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

const bornYear = () => new Date().getFullYear() - age

const bornYear = () => {
  return new Date().getFullYear() - age
}

إذًا تُسهّل عملية التفكيك إسناد القيم للمتغيّرات، حيث نستخلص قيم خصائص الكائنات ونضعها في متغيّرات منفصلة:

const Hello = (props) => {
  const { name, age } = props  const bornYear = () => new Date().getFullYear() - age

  return (
    <div>
      <p>Hello {name}, you are {age} years old</p>
      <p>So you were probably born in {bornYear()}</p>
    </div>
  )
}

فلو افترضنا أنّ الكائن الذي نفككه يمتلك القيم التالية:

props = {
  name: 'Arto Hellas',
  age: 35,
}

ستُسنِد العبارة const { name, age } = props القيمة "Arto Hellas" إلى المتغيّر name والقيمة "35" إلى المتغيّر age. سنتعمق قليلًا في فكرة التفكيك:

const Hello = ({ name, age }) => {  
  const bornYear = () => new Date().getFullYear() - age

  return (
    <div>
      <p>
        Hello {name}, you are {age} years old
      </p>
      <p>So you were probably born in {bornYear()}</p>
    </div>
  )
}

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

const Hello = (props) => {
  const { name, age } = props

بل أسندت قيمتي الخاصيتين مباشرة إلى المتغيّرات بتفكيك الكائن props أثناء تمريره كمُعامل لدالة المكوًن:

const Hello = ({ name, age }) => {

إعادة تصيير الصفحة Page re-rendering

لم تطرأ أية تغييرات على مظهر التطبيقات التي كتبناها حتى الآن. ماذا لو أردنا أن ننشئ عدادًا تزداد قيمته مع الوقت أو عند النقر على زر؟ لنبدأ بالشيفرة التالية:

const App = (props) => {
  const {counter} = props
  return (
    <div>{counter}</div>
  )
}

let counter = 1

ReactDOM.render(
  <App counter={counter} />, 
  document.getElementById('root')
)

تُمرَّر قيمة العدّاد إلى المكوّن App عبر الخاصيّة counter، ثم سيعمل على تصييرها على الشاشة. لكن ما الذي سيحدث لو تغيرت قيمة counter؟ حتى لو زدنا قيمتها تدريجيًا كالتالي:

counter += 1

فلن يصيّر المكوّن القيمة الجديدة على الشاشة. لحل المشكلة، علينا استدعاء التابع ReactDOM.render حتى يُعاد تصيير الصفحة مجددًا بالطريقة التالية:

const App = (props) => {
  const { counter } = props
  return (
    <div>{counter}</div>
  )
}

let counter = 1

const refresh = () => {
  ReactDOM.render(<App counter={counter} />, 
  document.getElementById('root'))
}

refresh()
counter += 1
refresh()
counter += 1
refresh()

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

setInterval(() => {
  refresh()
  counter += 1
}, 1000)

وأخيرًا لا تكرر استدعاء التابع ReactDOM.render، ولا ننصحك بذلك، لأنك ستتعلم لاحقًا طريقةً أفضل.

مكوّن متغير الحالات Stateful component

تبدو جميع المكوّنات التي تعاملنا معها حتى الآن بسيطة من مبدأ أنها لم تحتوي على حالات قد تتغير خلال فترة عمل المكوّن، لذلك سنضيف الآن حالة إلى المكوّن App بمساعدة أحد خطافات React وهو state hook. فلنقم إذًا بتعديل التطبيق على النحو:

import React, { useState } from 'react'import ReactDOM from 'react-dom'

const App = () => {
  const [ counter, setCounter ] = useState(0)
  setTimeout(    () => setCounter(counter + 1),    1000  )
  return (
    <div>{counter}</div>
  )
}

ReactDOM.render(
  <App />, 
  document.getElementById('root')
)

يُدرِج التطبيق في السطر الأول الدالة useState باستخدام الكلمة import:

import React, { useState } from 'react'

تبدأ التعليمات داخل دالة المكوّن باستدعاء دالة أخرى كما يلي:

const [ counter, setCounter ] = useState(0)

تضيف الدالة المستدعاة useState "حالة" وتصيّرها بعد إعطائها قيمة ابتدائية تساوي 0. بعدها تعيد الدالة مصفوفة تضم عنصرين تسندهما إلى المتغيّرين counter وsetCounter مستخدمةً الإسناد بالتفكيك. سيحمل المتغيّر counter القيمة الابتدائية للحالة وهي 0، بينما سيحمل setCounter القيمة التي تعيدها الدالة المغيّرة للحالة. يستدعي التطبيق بعد ذلك الدالة setTimeout ويمرر لها مُعاملان أحدهما دالة لزيادة قيمة العدّاد، والآخر لتحديد وقت الانتهاء (timeout) بثانية واحدة:

setTimeout(
  () => setCounter(counter + 1),
  1000
)

تُستدعى الدالة setCounter التي مُررت كمُعامل للدالة setTimeout بعد ثانية من استدعاء الأخيرة.

() => setCounter(counter + 1)

تعيد React تصييرالمكوّن بعد استدعاء الدالة setCounter والتي تعتبر هنا الدالة المغيّرة للحالة (كونها غيرت قيمة العدّاد)، ويعني هذا إعادة تنفيذ الشيفرة في دالة المكوّن:

(props) => {
  const [ counter, setCounter ] = useState(0)

  setTimeout(
    () => setCounter(counter + 1),
    1000
  )

  return (
    <div>{counter}</div>
  )
}

عندما تُنفَّذ دالة المكوّن ثانيةً، سيستدعي الدالة useState ثم يعيد القيمة الجديدة للحالة:1. وكذلك الأمر سيُستدعى setTimeout مجددًا والذي سيستدعي بدوره setCounter بعد انقضاء ثانية وسيزيد العدّاد هنا بمقدار 1 ليصبح 2 بعد أن أصبحت قيمة counter تساوي 1.

() => setCounter(2)

خلال هذه العملية ستظهر على الشاشة القيمة الأقدم للعدّاد. وهكذا سيعاد تصيير الكائن في كل مرة تغيّر فيها الدالة setCounter حالة المكوّن، وستزداد قيمة العدّاد مع تكرار العملية طالما أن التطبيق يعمل.

إن لم تحدث عملية إعادة التصيير في الوقت المحدد أو ظننت أنها تحصل في توقيت خاطئ، يمكنك تنقيح (Debug) التطبيق بإظهار قيم المتغيّرات على الطرفية بالطريقة التالية:

const App = () => {
  const [ counter, setCounter ] = useState(0)

  setTimeout(
    () => setCounter(counter + 1),
    1000
  )

  console.log('rendering...', counter)
  return (
    <div>{counter}</div>
  )
}

فمن السهل عندها تتبع ومراقبة استدعاءات دالة التصيير:

follow_render_function_01.png

معالجة الأحداث Event handling

أشرنا في القسم 0 في مواضع عدة إلى معالجات الأحداث، وهي دوال تُستدعَى عندما يقع حدث ما في زمن التشغيل. فعندما يتعامل المستخدم مع العناصر المختلفة لصفحة الويب، سيحرّض ذلك مجموعة مختلفة من الأحداث. لنعدّل تطبيقنا قليلًا بحيث تزداد قيمة العدّاد عندما ينقر المستخدم على زر، سنستخدم هنا العنصر button. تدعم هذه العناصر ما يسمى بأحداث الفأرة (mouse events)، وأكثر هذه الأحداث شيوعًا هو حدث النقر على الزر(click). يُسجَّل معالج حدث النقر في React كالتالي:

const App = () => {
  const [ counter, setCounter ] = useState(0)

  const handleClick = () => {    console.log('clicked')  }
  return (
    <div>
      <div>{counter}</div>
      <button onClick={handleClick}>
        plus
      </button>
    </div>
  )
}

نحدد من خلال الصفة onClick للعنصر button أن الدالة handleClick ستتولى معالجة حدث النقر على زر الفأرة. وهكذا ستُستدعَى الدالة handleClick في كل مرة ينقر فيها المستخدم على الزر plus، وستظهر الرسالة "clicked" على طرفية المتصفح. كما يُعرّف معالج الحدث أيضًا باسناده مباشرة إلى الصفة onClick:

const App = () => {
  const [ counter, setCounter ] = useState(0)

  return (
    <div>
      <div>{counter}</div>
      <button onClick={() => console.log('clicked')}>
        plus
      </button>
    </div>
  )
}

وبتغيير الشيفرة التي ينفذها معالج الحدث لتصبح على النحو:

<button onClick={() => setCounter(counter + 1)}>
  plus
</button>

سنحصل على السلوك المطلوب وهو زيادة العدّاد بمقدار 1 وإعادة تصييرالمكوّن. سنضيف الآن زرًا آخر لتصفير العدّاد:

const App = () => {
  const [ counter, setCounter ] = useState(0)

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

وهكذا سيكون تطبيقنا جاهزًا الآن.

معالج الحدث هو دالة

لقد عرفنا سابقًا معالج الحدث كقيمة للصفة onClick لعنصر الزر (button):

<button onClick={() => setCounter(counter + 1)}> 
  plus
</button>

لكن ماذا لو عرّفنا معالج الحدث بشكل أبسط كما يلي:

<button onClick={setCounter(counter + 1)}> 
  plus
</button>

سيدمر هذا العمل التطبيق:

re-render_error_02.png

ماذا حدث؟ يفترض أن يكون معالج الحدث دالة أو مرجعًا إلى دالة، فعندما نعرّفه على النحو:

<button onClick={setCounter(counter + 1)}>

يمثل معالج الحدث بهذه الطريقة استدعاءً لدالة، وقد يكتب بالشكل السابق، لكن ليس في حالتنا الخاصة هذه. حيث تكون قيمة counter في البداية 0، ثم تُصيّر React التابع للمرة الأولى مستدعيةً الدالة (setCounter(0+1 التي ستُغير الحالة من 0 إلى 1، وفي نفس الوقت تعيد React تصييرالمكوّن مستدعيةً من جديدsetCounter، وستتغير الحالة مجددًا يعقبها إعادة تصيير وهكذا. لنعد الآن إلى تعريف معالج الحدث بالشكل الصحيح:

<button onClick={() => setCounter(counter + 1)}> 
  plus
</button>

تمتلك الآن الصفة onClick التي تحدد ما يجري عند النقر على الزر، القيمة الناتجة عن تنفيذ (setCounter(counter+1)<=() وبذلك تزداد قيمة العدّاد فقط عندما ينقر المستخدم على الزر. لا يعتبر تعريف معالجات الأحداث باستخدام قوالب JSX فكرة جيدة. لا بأس بذلك في حالتنا هذه لأن معالج الحدث بسيط جدًا. لنقم بفصل معالج الحدث إلى عدة دوال:

const App = () => {
  const [ counter, setCounter ] = useState(0)

  const increaseByOne = () => setCounter(counter + 1)    
  const setToZero = () => setCounter(0)

  return (
    <div>
      <div>{counter}</div>
      <button onClick={increaseByOne}>
        plus
      </button>
      <button onClick={setToZero}>
        zero
      </button>
    </div>
  )
}

لقد عُرّف معالج الحدث في الشيفرة السابقة بشكلٍ صحيح، حيث أُسند للصفة onClick متغيّر يتضمن مرجعًا لدالة:

<button onClick={increaseByOne}> 
  plus
</button>

تمرير الحالة إلى المكوّنات الأبناء (child components)

يفضل عند كتابة مكونات React أن تكون صغيرة وقابلة للاستخدام المتكرر ضمن التطبيق وحتى عبر المشاريع المختلفة. لنُعِد صياغة تطبيقنا إذًا حتى يضم ثلاث مكونات أصغر، الأول لإظهار العدّاد والآخرين للزرين. لنبدأ بالمكون Display الذي سيكون مسؤولًا عن إظهار قيمة العدّاد على الشاشة. من الأفضل الإبقاء على حالة التطبيق ضمن مكونات المستوى الأعلى (lift the state up)، حيث ينص التوثيق على ما يلي:

اقتباس

عندما تحتاج عدة مكونات لإظهار نفس البيانات التي تتغير، ننصح بالاحتفاظ بالحالة المشتركة ضمن المكوّن الأب الأقرب لهذه المكوّنات

لذا سنضع حالة التطبيق في المكوّن App، ونمرره نزولًا إلى المكوّن Display عبر الخصائص:

const Display = (props) => {
  return (
    <div>{props.counter}</div>
  )
}

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

const App = () => {
  const [ counter, setCounter ] = useState(0)

  const increaseByOne = () => setCounter(counter + 1)
  const setToZero = () => setCounter(0)

  return (
    <div>
      <Display counter={counter}/>
      <button onClick={increaseByOne}>
        plus
      </button>
      <button onClick={setToZero}> 
        zero
      </button>
    </div>
  )
}

سيجري كل شيء بشكل طبيعي، فحين ننقر الزر سيعاد تصيير المكوّن App وكذلك المكوّن الابنDisplay. سننشئ تاليًا المكوّن Button ليمثل أزرار التطبيق. علينا أن نمرر معالجات الأحداث وعنوان الزر عبر خصائص المكوّن:

const Button = (props) => {
  return (
    <button onClick={props.handleClick}>
      {props.text}
    </button>
  )
}

سيبدو المكوّن App الآن بهذا الشكل:

const App = () => {
  const [ counter, setCounter ] = useState(0)

  const increaseByOne = () => setCounter(counter + 1)
  const decreaseByOne = () => setCounter(counter - 1)
  const setToZero = () => setCounter(0)

  return (
    <div>
      <Display counter={counter}/>
      <Button
        handleClick={increaseByOne}
        text='plus'
      />      
      <Button
        handleClick={setToZero}
        text='zero'
      />           
      <Button
        handleClick={decreaseByOne}
        text='minus'
      />               
    </div>
  )
}

طالما أننا حصلنا على المكوّن Button الذي يمكن استخدامه بسهولة أينما كان، سنزيد من إمكانية التطبيق بإضافة زر جديد ليُنقص قيمة العدّاد. يمرر معالج الحدث إلى المكوّن Button عبر الخاصية handleClick. ليس مهمًا الاسم الذي اطلقناه على الخاصية، لكننا لم نختره عشوائيًا أيضًا، فقد اقترح أسلوب التسمية هذا في الدورة التعليمية الرسمية لمكتبة React.

تغّير الحالة يستوجب إعادة التصيير

لنستعرض المبدأ الرئيسي لعمل التطبيق مرّة أخرى. عندما يبدأ العمل ستُنفَّذ شيفرة المكوّن App. تستخدم تلك الشيفرة الخطاف useState لبناء حالة التطبيق وتحدد القيمة البدائية للمتغيّر counter. يضم هذا المكوّن مكوّنًا آخر هو Display الذي يعرض القيمة البدائية 0 للعداد، كما يضم ثلاثة مكونات Button، ولكل ٍّ منها معالج حدث يغيّر حالة العدّاد. فعندما يُنقر أي زر سيُنفَّذ معالج الحدث المقابل والذي يستخدم الدالة setCounter لتغيير حالة المكوّن App. ودائمًا استدعاء الدالة التي تغير حالة المكوّن يستوجب إعادة التصيير. فلو نقر المستخدم زر الزيادة (زر عنوانه plus)، سيغيّر معالج الحدث الخاص به قيمة العدّاد إلى 1، وسيعاد تصيير المكوّن App. سيستقبل المكوّن Display أيضًا قيمة العدّاد الجديدة كخاصية، كما سيُزوَّد المكوّن Button بمعالج حدث سيُستخدم لتغيير حالة العدّاد.

إعادة تصميم المكوّنات Refactoring the components

يظهر المكوِّن الذي يعرض قيمة العدّاد على الصفحة بالشكل التالي:

const Display = (props) => {
  return (
    <div>{props.counter}</div>
  )
}

يستخدم المكوّن عمليًا الحقل counter فقط من الخصائص، لهذا يمكن تبسيطه باستخدام التفكيك كما يلي:

const Display = ({ counter }) => {
  return (
    <div>{counter}</div>
  )
}

يضم التابع الذي يعرّف المكوّن العبارة التي تعيد نتيجة التنفيذ فقط، لذا يمكننا كتابته بالطريقة المختصرة التي تقدمها الدوال السهمية:

const Display = ({ counter }) => <div>{counter}</div>

لنبسط أيضًا كتابة المكوّن Button:

const Button = (props) => {
  return (
    <button onClick={props.handleClick}>
      {props.text}
    </button>
  )
}

وهكذا نرى فائدة التفكيك في استخدام الحقل المطلوب فقط من الخصائص، وسهولة كتابة دوال بشكل مختصر بالاستفادة من الدوال السهمية:

const Button = ({ handleClick, text }) => (
  <button onClick={handleClick}>
    {text}
  </button>
)

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





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


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



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن