لنعد إلى العمل مع 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> ) }
فمن السهل عندها تتبع ومراقبة استدعاءات دالة التصيير:
معالجة الأحداث 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>
سيدمر هذا العمل التطبيق:
ماذا حدث؟ يفترض أن يكون معالج الحدث دالة أو مرجعًا إلى دالة، فعندما نعرّفه على النحو:
<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
تم التعديل في بواسطة جميل بيلوني
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.