حالة المكوّن ومعالجات الأحداث
لنعد إلى العمل مع 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) => {
// highlight-start
const bornYear = () => {
const yearNow = new Date().getFullYear()
return yearNow - props.age
}
// highlight-end
return (
<div>
<p>
Hello {props.name}, you are {props.age} years old
</p>
<p>So you were probably born in {bornYear()}</p>
</div>
)
}
منطق تخمين سنة الميلاد مُغلَّف داخل دالة خاصة به، تُستدعى عند عرض المكوّن.
لا حاجة لتمرير عمر الشخص صراحةً كوسيط إلى هذه الدالة، لأن الدالة يمكنها الوصول مباشرةً إلى كل الـ props الممرَّرة إلى المكوّن.
إذا تأملنا الشيفرة الحالية، نلاحظ أن الدالة المساعدة معرَّفة داخل دالة أخرى تحدّد سلوك المكوّن. في برمجة Java، قد يكون تعريف دالة داخل دالة أخرى معقّداً وغير شائع. أما في JavaScript، فتعريف الدوال داخل الدوال ممارسة شائعة وفعّالة.
التفكيك (destructuring)
قبل أن نمضي قدماً، سنلقي نظرة على ميزة صغيرة لكن مفيدة من لغة JavaScript أُضيفت في مواصفة ES6، وتتيح لنا تفكيك القيم من الكائنات والمصفوفات عند الإسناد.
في شيفرتنا السابقة، كان علينا الإشارة إلى البيانات الممرَّرة إلى مكوّننا بصيغة props.name و_props.age_. ومن بين هذين التعبيرين، اضطررنا لتكرار props.age مرتين في شيفرتنا.
بما أن props كائن
props = {
name: 'Arto Hellas',
age: 35,
}
يمكننا تبسيط مكوّننا بإسناد قيم الخصائص مباشرةً إلى متغيرين هما name و_age_ يمكننا بعدها استخدامهما في شيفرتنا:
const Hello = (props) => {
// highlight-start
const name = props.name
const age = props.age
// highlight-end
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) => {
// highlight-start
const { name, age } = props
// highlight-end
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>
)
}
الـ props الممرَّرة إلى المكوّن تُفكَّك الآن مباشرةً إلى المتغيرين name و_age_.
وهذا يعني أنه بدلاً من إسناد كائن الـ props كاملاً إلى متغير يُسمى props ثم إسناد خصائصه إلى المتغيرين name و_age_
const Hello = (props) => {
const { name, age } = props
نُسند قيم الخصائص مباشرةً إلى المتغيرات عبر تفكيك كائن الـ props الممرَّر إلى دالة المكوّن كوسيط:
const Hello = ({ name, age }) => {
إعادة عرض الصفحة
حتى هذه اللحظة، كانت تطبيقاتنا ساكنة — يبقى مظهرها دون تغيير بعد العرض الأولي. لكن ماذا لو أردنا إنشاء عدّاد تزداد قيمته بمرور الوقت أو عند النقر على زر؟
لنبدأ بما يلي. يصبح الملف App.jsx:
const App = (props) => {
const {counter} = props
return (
<div>{counter}</div>
)
}
export default App
ويصبح الملف main.jsx:
import ReactDOM from 'react-dom/client'
import App from './App'
let counter = 1
ReactDOM.createRoot(document.getElementById('root')).render(
<App counter={counter} />
)
يُمنح مكوّن App قيمة العدّاد عبر الـ prop المسمى counter. ويعرض هذا المكوّن القيمة على الشاشة. ماذا يحدث عندما تتغير قيمة counter؟ حتى لو أضفنا ما يلي
counter += 1
فلن يُعاد عرض المكوّن. يمكننا جعل المكوّن يُعاد عرضه باستدعاء الدالة render مرة ثانية، مثلاً بالطريقة التالية:
let counter = 1
const root = ReactDOM.createRoot(document.getElementById('root'))
const refresh = () => {
root.render(
<App counter={counter} />
)
}
refresh()
counter += 1
refresh()
counter += 1
refresh()
لُفّ أمر إعادة العرض داخل الدالة refresh لتقليل كمية الشيفرة المنسوخة.
الآن يُعرض المكوّن ثلاث مرات، أولاً بالقيمة 1 ثم 2 وأخيراً 3. لكن القيمتين 1 و2 تُعرضان على الشاشة لمدة قصيرة جداً بحيث لا يمكن ملاحظتهما.
يمكننا تنفيذ وظيفة أكثر تشويقاً بقليل عبر إعادة العرض وزيادة العدّاد كل ثانية باستخدام setInterval:
setInterval(() => {
refresh()
counter += 1
}, 1000)
تكرار استدعاء الدالة render ليس الطريقة الموصى بها لإعادة عرض المكوّنات. سنقدّم لاحقاً طريقة أفضل لتحقيق هذا الأثر.
مكوّن ذو حالة
كانت جميع مكوّناتنا حتى الآن بسيطة بمعنى أنها لم تكن تحتوي على أي حالة يمكن أن تتغير خلال دورة حياة المكوّن.
لنضف الآن حالة إلى مكوّن App في تطبيقنا بمساعدة خطاف الحالة في React.
سنغيّر التطبيق كما يلي. يعود الملف main.jsx إلى:
import ReactDOM from 'react-dom/client'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')).render(<App />)
ويتغير الملف App.jsx إلى ما يلي:
import { useState } from 'react'
const App = () => {
const [ counter, setCounter ] = useState(0)
// highlight-start
setTimeout(
() => setCounter(counter + 1),
1000
)
// highlight-end
return (
<div>{counter}</div>
)
}
export default App
في السطر الأول، يستورد الملف الدالة useState:
import { useState } from 'react'
يبدأ جسم الدالة الذي يعرّف المكوّن باستدعاء الدالة:
const [ counter, setCounter ] = useState(0)
يضيف استدعاء الدالة حالة إلى المكوّن ويعرضه مهيَّأً بالقيمة صفر. وتُعيد الدالة مصفوفة تحتوي على عنصرين. نُسند العنصرين إلى المتغيرين counter و_setCounter_ باستخدام صيغة إسناد التفكيك التي عرضناها سابقاً.
يُسند إلى المتغير counter القيمة الأولية لـالحالة، وهي صفر. ويُسند إلى المتغير setCounter دالة ستُستخدم لـتعديل الحالة.
يستدعي التطبيق الدالة setTimeout ويمرّر إليها وسيطين: دالة لزيادة حالة العدّاد ومهلة زمنية مقدارها ثانية واحدة:
setTimeout(
() => setCounter(counter + 1),
1000
)
تُستدعى الدالة الممرَّرة كوسيط أول إلى الدالة setTimeout بعد ثانية واحدة من استدعاء الدالة setTimeout
() => setCounter(counter + 1)
عند استدعاء الدالة المعدِّلة للحالة setCounter، يُعيد React عرض المكوّن، ما يعني إعادة تنفيذ جسم دالة المكوّن:
() => {
const [ counter, setCounter ] = useState(0)
setTimeout(
() => setCounter(counter + 1),
1000
)
return (
<div>{counter}</div>
)
}
في المرة الثانية التي تُنفَّذ فيها دالة المكوّن، تستدعي الدالة useState وتُعيد القيمة الجديدة للحالة: 1. كما أن إعادة تنفيذ جسم الدالة تجري استدعاءً جديداً للدالة setTimeout، التي تنفّذ المهلة الزمنية مقدارها ثانية وتزيد حالة counter مرة أخرى. ولأن قيمة المتغير counter هي 1، فإن زيادتها بمقدار 1 تكافئ فعلياً تعبيراً يضبط قيمة counter على 2.
() => setCounter(2)
وفي الوقت نفسه، تُعرض على الشاشة القيمة القديمة للمتغير counter — «1».
في كل مرة تعدّل فيها setCounter الحالة، تتسبب في إعادة عرض المكوّن. وستزداد قيمة الحالة مرة أخرى بعد ثانية واحدة، وسيستمر هذا التكرار طالما بقي التطبيق قيد التشغيل.
إذا لم يُعرض المكوّن عندما تظن أنه ينبغي أن يُعرض، أو إذا عُرض في «الوقت الخطأ»، يمكنك تصحيح أخطاء التطبيق بطبع قيم متغيرات المكوّن في وحدة التحكم. وإذا أضفنا الإضافات التالية إلى شيفرتنا:
const App = () => {
const [ counter, setCounter ] = useState(0)
setTimeout(
() => setCounter(counter + 1),
1000
)
console.log('rendering...', counter)
return (
<div>{counter}</div>
)
}
يصبح من السهل متابعة وتتبّع الاستدعاءات التي تجريها دالة العرض الخاصة بمكوّن App:

هل كانت وحدة تحكم المتصفح مفتوحة؟ إن لم تكن كذلك، فعِدنا بأن هذه آخر مرة نحتاج فيها إلى تذكيرك بذلك.
معالجة الأحداث
سبق أن ذكرنا معالجات الأحداث التي تُسجَّل لتُستدعى عند وقوع أحداث معينة عدة مرات في الجزء 0. ويمكن أن يؤدي تفاعل المستخدم مع العناصر المختلفة لصفحة ويب إلى إطلاق مجموعة من الأحداث المتنوعة.
لنغيّر التطبيق بحيث تحدث زيادة العدّاد عند نقر المستخدم على زر، وهو أمر يُنفَّذ بعنصر button.
تدعم عناصر الزر ما يُسمى أحداث الفأرة، وأكثرها شيوعاً هو الحدث click. ويمكن إطلاق حدث النقر على زر أيضاً بلوحة المفاتيح أو شاشة اللمس رغم الاسم حدث الفأرة.
في React، تجري تسجيل دالة معالج حدث للحدث click هكذا:
const App = () => {
const [ counter, setCounter ] = useState(0)
// highlight-start
const handleClick = () => {
console.log('clicked')
}
// highlight-end
return (
<div>
<div>{counter}</div>
// highlight-start
<button onClick={handleClick}>
plus
</button>
// highlight-end
</div>
)
}
نضبط قيمة خاصية onClick للزر على مرجع إلى الدالة handleClick المعرَّفة في الشيفرة.
الآن كل نقرة على زر plus تؤدي إلى استدعاء الدالة handleClick، ما يعني أن كل حدث نقر سيطبع رسالة 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>
نحقّق السلوك المطلوب، أي تزداد قيمة counter بمقدار واحد ويُعاد عرض المكوّن.
لنضف أيضاً زراً لإعادة تعيين العدّاد:
const App = () => {
const [ counter, setCounter ] = useState(0)
return (
<div>
<div>{counter}</div>
<button onClick={() => setCounter(counter + 1)}>
plus
</button>
// highlight-start
<button onClick={() => setCounter(0)}>
zero
</button>
// highlight-end
</div>
)
}
تطبيقنا جاهز الآن!
معالج الحدث دالة
نعرّف معالجات الأحداث لأزرارنا حيث نصرّح بخصائص onClick الخاصة بها:
<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) ويغيّر قيمة حالة المكوّن إلى 1. سيؤدي هذا إلى إعادة عرض المكوّن، وسينفّذ React استدعاء الدالة setCounter مرة أخرى، وستتغير الحالة مؤديةً إلى إعادة عرض أخرى...
لنعرّف معالجات الأحداث كما فعلنا سابقاً:
<button onClick={() => setCounter(counter + 1)}>
plus
</button>
الآن أصبحت خاصية الزر التي تحدّد ما يحدث عند النقر على الزر — onClick — تحمل القيمة () => setCounter(counter + 1). ولا تُستدعى الدالة setCounter إلا عندما ينقر المستخدم على الزر.
عادةً ليس من الجيد تعريف معالجات الأحداث داخل قوالب JSX. لكن الأمر مقبول هنا لأن معالجات الأحداث لدينا بسيطة جداً.
لنفصل معالجات الأحداث في دوال منفصلة على أي حال:
const App = () => {
const [ counter, setCounter ] = useState(0)
// highlight-start
const increaseByOne = () => setCounter(counter + 1)
const setToZero = () => setCounter(0)
// highlight-end
return (
<div>
<div>{counter}</div>
<button onClick={increaseByOne}>
plus
</button>
<button onClick={setToZero}>
zero
</button>
</div>
)
}
هنا، عُرّفت معالجات الأحداث بشكل صحيح. قيمة خاصية onClick هي متغير يحتوي على مرجع إلى دالة:
<button onClick={increaseByOne}>
plus
</button>
تمرير الحالة إلى المكوّنات الابنة
يُستحسن كتابة مكوّنات React صغيرة وقابلة لإعادة الاستخدام في أنحاء التطبيق وحتى في مشاريع أخرى. لنُعد هيكلة تطبيقنا بحيث يتكوّن من ثلاثة مكوّنات أصغر: مكوّن لعرض العدّاد ومكوّنان للأزرار.
لننفّذ أولاً مكوّن Display المسؤول عن عرض قيمة العدّاد.
من أفضل الممارسات في React رفع الحالة لأعلى في التسلسل الهرمي للمكوّنات. تقول التوثيقات:
غالباً ما تحتاج عدة مكوّنات إلى عكس البيانات المتغيرة نفسها. نوصي برفع الحالة المشتركة إلى أقرب سلف مشترك لها.
فلنضع إذن حالة التطبيق في مكوّن App ونمرّرها إلى مكوّن Display عبر props:
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 لأزرار تطبيقنا. علينا تمرير معالج الحدث وكذلك نص الزر عبر props المكوّن:
const Button = (props) => {
return (
<button onClick={props.onClick}>
{props.text}
</button>
)
}
أصبح مكوّن App لدينا الآن هكذا:
const App = () => {
const [ counter, setCounter ] = useState(0)
const increaseByOne = () => setCounter(counter + 1)
//highlight-start
const decreaseByOne = () => setCounter(counter - 1)
//highlight-end
const setToZero = () => setCounter(0)
return (
<div>
<Display counter={counter}/>
// highlight-start
<Button
onClick={increaseByOne}
text='plus'
/>
<Button
onClick={setToZero}
text='zero'
/>
<Button
onClick={decreaseByOne}
text='minus'
/>
// highlight-end
</div>
)
}
وبما أنه أصبح لدينا مكوّن Button سهل إعادة الاستخدام، نفّذنا أيضاً وظيفة جديدة في تطبيقنا بإضافة زر يمكن استخدامه لإنقاص العدّاد.
يُمرَّر معالج الحدث إلى مكوّن Button عبر الـ prop المسمى onClick. عند إنشاء مكوّناتك الخاصة، يمكنك نظرياً اختيار اسم الـ prop بحرية. غير أن اختيارنا لاسم معالج الحدث لم يكن اعتباطياً بالكامل.
يقترح الدرس التعليمي الرسمي من React نفسه: «في React، من المعتاد استخدام أسماء من نمط onSomething للـ props التي تستقبل دوال تتعامل مع الأحداث، و_handleSomething_ لتعريفات الدوال الفعلية التي تعالج تلك الأحداث».
التغييرات في الحالة تسبب إعادة العرض
لنستعرض مرة أخرى المبادئ الأساسية لعمل التطبيق.
عند بدء التطبيق، تُنفَّذ الشيفرة الموجودة في App. تستخدم هذه الشيفرة الخطاف useState لإنشاء حالة التطبيق، مع ضبط قيمة أولية للمتغير counter. يحتوي هذا المكوّن على مكوّن Display — الذي يعرض قيمة العدّاد، 0 — وثلاثة مكوّنات Button. ولجميع الأزرار معالجات أحداث تُستخدم لتغيير حالة العدّاد.
عند النقر على أحد الأزرار، يُنفَّذ معالج الحدث. يغيّر معالج الحدث حالة مكوّن App باستخدام الدالة setCounter. استدعاء دالة تغيّر الحالة يتسبب في إعادة عرض المكوّن.
لذا، إذا نقر المستخدم على زر plus، يغيّر معالج حدث الزر قيمة counter إلى 1، ويُعاد عرض مكوّن App. ويؤدي هذا إلى إعادة عرض مكوّناته الفرعية Display و_Button_ أيضاً. يستقبل Display القيمة الجديدة للعدّاد، 1، كـ props. وتستقبل مكوّنات Button معالجات أحداث يمكن استخدامها لتغيير حالة العدّاد.
وللتأكد من فهم كيفية عمل البرنامج، لنضف إليه بعض عبارات console.log
const App = () => {
const [counter, setCounter] = useState(0)
console.log('rendering with counter value', counter)
const increaseByOne = () => {
console.log('increasing, value before', counter)
setCounter(counter + 1)
}
const decreaseByOne = () => {
console.log('decreasing, value before', counter)
setCounter(counter - 1)
}
const setToZero = () => {
console.log('resetting to zero, value before', counter)
setCounter(0)
}
return (
<div>
<Display counter={counter} />
<Button onClick={increaseByOne} text="plus" />
<Button onClick={setToZero} text="zero" />
<Button onClick={decreaseByOne} text="minus" />
</div>
)
}
لنرَ الآن ما يُطبع في وحدة التحكم عند الضغط على الأزرار plus وzero وminus:

لا تحاول أبداً تخمين ما تفعله شيفرتك. من الأفضل استخدام console.log ورؤية ما تفعله بعينيك.
إعادة هيكلة المكوّنات
المكوّن الذي يعرض قيمة العدّاد هو كما يلي:
const Display = (props) => {
return (
<div>{props.counter}</div>
)
}
يستخدم المكوّن فقط الحقل counter من props الخاصة به. وهذا يعني أنه يمكننا تبسيط المكوّن باستخدام التفكيك، هكذا:
const Display = ({ counter }) => {
return (
<div>{counter}</div>
)
}
تحتوي الدالة التي تعرّف المكوّن على عبارة return فقط، لذا يمكننا تعريف الدالة باستخدام الصيغة الأكثر إيجازاً لدوال السهم:
const Display = ({ counter }) => <div>{counter}</div>
يمكننا تبسيط مكوّن Button أيضاً.
const Button = (props) => {
return (
<button onClick={props.onClick}>
{props.text}
</button>
)
}
يمكننا استخدام التفكيك للحصول على الحقول المطلوبة فقط من props، واستخدام الصيغة الأكثر إيجازاً لدوال السهم:
const Button = ({ onClick, text }) => <button onClick={onClick}>{text}</button>
تنجح هذه الطريقة لأن المكوّن يحتوي على عبارة return واحدة فقط، ما يجعل استخدام صيغة دالة السهم المختصرة ممكناً.