خطافات React للمبتدئين: دليل عملي لفهم useState و useEffect
كثيرًا ما يتساءل المطورون، “ما هي خطافات React بالضبط؟” هذا السؤال يتبادر إلى الذهن غالبًا عندما نعتقد أننا ألممنا بجميع أساسيات React. هذه هي طبيعة عمل مطور الواجهات الأمامية؛ عالم يتغير باستمرار. وهنا يأتي دور الخطافات (Hooks).
من الرائع دائمًا تعلم الجديد، أليس كذلك؟ بالتأكيد! ولكن أحيانًا يجب أن نسأل أنفسنا: “لماذا؟ ما الفائدة من هذا المفهوم الجديد؟ هل أنا مُلزم بتعلمه؟” بالنسبة لـ Hooks، الإجابة هي “ليس فورًا”. إذا كنت تتعلم React وتستخدم المكونات المبنية على الفئات (class-based components) حتى الآن، فلا داعي للاندفاع للانتقال إلى الخطافات. إنها اختيارية ويمكن أن تعمل جنبًا إلى جنب مع مكوناتك الحالية. ألا تكره الاضطرار إلى إعادة كتابة قاعدة بياناتك بالكامل لتشغيل ميزة جديدة؟
على أي حال، إليك بعض الأسباب التي أدت إلى تقديم الخطافات في المقام الأول، ولماذا أوصي المبتدئين بتعلمها:
لماذا ظهرت خطافات React؟ الأسباب الجوهرية
1. استخدام الحالة (State) في المكونات الوظيفية (Functional Components)
قبل ظهور الخطافات، لم يكن بإمكاننا استخدام الحالة (state) داخل المكونات الوظيفية. هذا يعني أنه إذا كان لديك مكون وظيفي مصمم بعناية ومختبر يحتاج فجأة إلى تخزين حالة ما، فستجد نفسك أمام مهمة مؤلمة تتمثل في إعادة هيكلة هذا المكون الوظيفي ليصبح مكونًا مبنيًا على الفئات (class component). يا للروعة! إن السماح باستخدام الحالة داخل المكونات الوظيفية يعني أننا لم نعد بحاجة إلى إعادة هيكلة مكوناتنا التقديمية (presentation components)، مما يوفر الكثير من الوقت والجهد.
2. التخلص من تعقيدات المكونات المبنية على الفئات (Class Components)
دعنا نعترف بذلك، تأتي المكونات المبنية على الفئات بكمية كبيرة من التعليمات البرمجية المكررة (boilerplate). المنشئات (constructors)، الربط (binding)، واستخدام الكلمة المفتاحية this في كل مكان. استخدام المكونات الوظيفية يزيل الكثير من هذه التعقيدات، مما يجعل التعليمات البرمجية أسهل في المتابعة والصيانة. هذا يقلل من فرص الأخطاء ويزيد من إنتاجية المطور.
3. تعليمات برمجية أكثر قابلية للقراءة والصيانة
بما أن الخطافات تتيح لنا استخدام المكونات الوظيفية، فهذا يعني وجود تعليمات برمجية أقل مقارنة بالمكونات المبنية على الفئات. هذا يجعل الكود أكثر قابلية للقراءة والفهم. الفكرة الأساسية هي أننا لم نعد بحاجة للقلق بشأن ربط دوالنا (binding functions) أو تذكر ما تشير إليه الكلمة المفتاحية this، وما إلى ذلك. يمكننا التركيز بشكل أكبر على كتابة منطق تطبيقنا بدلاً من إدارة تعقيدات البنية.
خطاف الحالة في React: useState
الحالة (State) هي حجر الزاوية في نظام React البيئي. دعنا نتعمق في عالم الخطافات بتقديم الخطاف الأكثر شيوعًا الذي ستعمل معه: useState(). لنلقِ نظرة أولاً على مكون مبني على الفئات يحتوي على حالة:
import React, { Component } from 'react';
import './styles.css';
class Counter extends Component {
state = {
count: this.props.initialValue,
};
setCount = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<h2>This is a counter using a class</h2>
<h1>{this.state.count}</h1>
<button onClick={this.setCount}>Click to Increment</button>
</div>
);
}
}
export default Counter;
باستخدام خطافات React، يمكننا إعادة كتابة هذا المكون وإزالة الكثير من التعليمات البرمجية الزائدة، مما يجعله أسهل في الفهم والصيانة:
import React, { useState } from 'react';
function CounterWithHooks(props) {
const [count, setCount] = useState(props.initialValue);
return (
<div>
<h2>This is a counter using hooks</h2>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>Click to Increment</button>
</div>
);
}
export default CounterWithHooks;
للوهلة الأولى، يبدو الكود أقل، ولكن ما الذي يحدث بالضبط؟
فهم بناء جملة useState
لقد رأينا أول خطاف لنا! تهانينا! لنفهم بناء الجملة:
const [count, setCount] = useState();
بشكل أساسي، تستخدم هذه الجملة تعيين التفكيك للمصفوفات (destructuring assignment for arrays). تمنحنا الدالة useState() شيئين:
- متغيرًا لتخزين قيمة الحالة، وفي هذه الحالة يُسمى
count. - دالة لتغيير هذه القيمة، وفي هذه الحالة تُسمى
setCount.
يمكنك تسمية هذين المتغيرين والدالة بأي اسم تريده:
const [myCount, setMyCount] = useState(0);
ويمكنك استخدامها في جميع أنحاء التعليمات البرمجية كمتغيرات/دوال عادية:
function CounterWithHooks() {
const [count, setCount] = useState(0); // تم تعيين قيمة ابتدائية هنا
return (
<div>
<h2>This is a counter using hooks</h2>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>Click to Increment</button>
</div>
);
}
لاحظ خطاف useState في الأعلى. نحن نعلن/نفكك شيئين:
count: قيمة ستحمل قيمة حالتنا.setCount: دالة ستغير متغيرcountالخاص بنا.
بينما نواصل تصفح الكود، سترى هذا السطر:
<h1>{count}</h1>
هذا مثال على كيفية استخدام متغير خطاف الحالة. داخل JSX الخاص بنا، نضع متغير count الخاص بنا داخل {} لتنفيذه كـ JavaScript، وبدوره يتم عرض قيمة count على الصفحة.
بمقارنة هذا بالطريقة القديمة “المبنية على الفئات” لاستخدام متغير الحالة:
<h1>{this.state.count}</h1>
ستلاحظ أننا لم نعد بحاجة للقلق بشأن استخدام this، مما يجعل حياتنا أسهل بكثير. على سبيل المثال، سيمنحنا محرر VS Code تحذيرًا إذا لم يتم تعريف {count}، مما يسمح لنا باكتشاف الأخطاء مبكرًا. بينما لن يعرف ما إذا كان {this.state.count} غير معرف حتى يتم تشغيل الكود.
إلى السطر التالي!
<button onClick={ () => setCount(count + 1 )}>Click to Increment</button>
هنا، نستخدم الدالة setCount (تذكر أننا فككنا/أعلنا عنها من خطاف useState()) لتغيير متغير count. عند النقر على الزر، نقوم بتحديث متغير count بمقدار 1. نظرًا لأن هذا تغيير في الحالة، فإنه يؤدي إلى إعادة عرض المكون (rerender)، ويقوم React بتحديث العرض بقيمة count الجديدة لنا. رائع!
كيف يمكنني تعيين الحالة الأولية؟
يمكنك تعيين الحالة الأولية عن طريق تمرير وسيطة إلى بناء جملة useState(). يمكن أن تكون هذه قيمة مبرمجة ثابتة (hardcoded value):
const [count, setCount] = useState(0);
أو يمكن أخذها من الخصائص (props):
const [count, setCount] = useState(props.initialValue);
سيؤدي هذا إلى تعيين قيمة count إلى أي قيمة تحملها props.initialValue. هذا يلخص استخدام useState(). جماله يكمن في أنه يمكنك استخدام متغيرات/دوال الحالة مثل أي متغير/دالة أخرى تكتبها بنفسك.
كيف أتعامل مع متغيرات الحالة المتعددة؟
هذا جانب آخر رائع في الخطافات. يمكننا الحصول على أي عدد نريده منها في المكون الواحد:
const [count, setCount] = useState(props.initialValue);
const [title, setTitle] = useState("This is my title");
const [age, setAge] = useState(25);
كما ترى، لدينا ثلاثة كائنات حالة منفصلة. إذا أردنا تحديث العمر على سبيل المثال، فإننا ببساطة نستدعي الدالة setAge(). وينطبق الشيء نفسه على count و title. لم نعد مقيدين بالطريقة القديمة المعقدة للمكونات المبنية على الفئات حيث كان لدينا كائن حالة واحد ضخم يتم تخزينه باستخدام setState():
this.setState({
count: props.initialValue,
title: "This is my title",
age: 25
})
خطاف التأثير في React: useEffect
ماذا عن تحديث الأشياء عندما تتغير الخصائص (props) أو الحالة (state)؟ عند استخدام الخطافات والمكونات الوظيفية، لم يعد لدينا وصول إلى دوال دورة حياة React مثل componentDidMount() و componentDidUpdate() وما إلى ذلك. لا داعي للذعر يا صديقي، فقد قدم لنا React خطافًا آخر يمكننا استخدامه: إنه useEffect()!
ما هي “التأثيرات الجانبية” (Side Effects)؟
خطاف التأثير (useEffect()) هو المكان الذي نضع فيه “التأثيرات الجانبية”. ما هي التأثيرات الجانبية بالضبط؟ دعنا نبتعد قليلًا عن المسار الرئيسي ونناقش ماهية التأثير الجانبي. سيساعدنا هذا في فهم ما يفعله useEffect() ولماذا هو مفيد.
التفسير التقني المبسط هو: “في البرمجة، التأثير الجانبي هو عندما يغير إجراء ما متغيرًا من خارج نطاقه”.
بمصطلحات React، هذا يعني “عندما تتغير متغيرات المكون أو حالته بناءً على شيء خارجي”. على سبيل المثال، يمكن أن يكون هذا:
- عندما يتلقى المكون خصائص جديدة تغير حالته.
- عندما يقوم المكون بإجراء استدعاء لواجهة برمجة تطبيقات (API call) ويفعل شيئًا بالاستجابة (مثل تغيير الحالة).
لماذا يطلق عليه “تأثير جانبي”؟ حسنًا، لا يمكننا التأكد من نتيجة الإجراء. لا يمكننا أبدًا أن نكون متأكدين بنسبة 100% من الخصائص التي سنتلقاها، أو ما ستكون عليه الاستجابة من استدعاء واجهة برمجة التطبيقات. ولا يمكننا التأكد من كيفية تأثير ذلك على مكوننا. بالتأكيد يمكننا كتابة كود للتحقق من الصحة والتعامل مع الأخطاء وما إلى ذلك، ولكن في النهاية لا يمكننا التأكد من الآثار الجانبية لهذه الأمور. على سبيل المثال، عندما نغير الحالة بناءً على شيء خارجي، يُعرف هذا بأنه تأثير جانبي.
بعد توضيح ذلك، دعنا نعود إلى React وخطاف useEffect!
عند استخدام المكونات الوظيفية، لم يعد لدينا وصول إلى دوال دورة الحياة مثل componentDidMount() و componentDidUpdate() وما إلى ذلك. لذلك، في الواقع (وهنا تكمن المفارقة اللفظية)، تحل خطافات useEffect محل خطافات دورة حياة React الحالية.
دعنا نقارن مكونًا مبنيًا على الفئات بكيفية استخدامنا لخطاف useEffect:
import React, { Component } from 'react';
class App extends Component {
componentDidMount() {
console.log('I have just mounted!');
}
render() {
return <div>Insert JSX here</div>;
}
}
والآن باستخدام useEffect():
import React, { useEffect } from 'react';
function App() {
useEffect(() => {
console.log('I have just mounted!');
});
return <div>Insert JSX here</div>;
}
قبل أن نواصل، من المهم معرفة أنه، بشكل افتراضي، يتم تشغيل خطاف useEffect في كل عملية عرض وإعادة عرض (render and re-render). لذلك، كلما تغيرت الحالة في مكونك أو تلقى مكونك خصائص جديدة، فإنه سيعاد عرضه ويتسبب في تشغيل خطاف useEffect مرة أخرى.
تشغيل التأثير مرة واحدة فقط (مكافئ componentDidMount)
إذًا، إذا كانت الخطافات تعمل في كل مرة يتم فيها عرض المكون، فكيف نضمن أن الخطاف يعمل مرة واحدة فقط عند تحميل المكون (mounting)؟ على سبيل المثال، إذا كان المكون يجلب بيانات من واجهة برمجة تطبيقات (API)، فلا نريد أن يحدث هذا في كل مرة يعاد فيها عرض المكون!
يأخذ خطاف useEffect() معلمة ثانية، وهي مصفوفة، تحتوي على قائمة الأشياء التي ستتسبب في تشغيل خطاف useEffect. عند تغييرها، ستؤدي إلى تشغيل خطاف التأثير. المفتاح لتشغيل التأثير مرة واحدة هو تمرير مصفوفة فارغة:
useEffect(() => {
console.log('This only runs once');
}, []);
هذا يعني أن خطاف useEffect سيعمل في العرض الأول كالمعتاد. ومع ذلك، عندما يعاد عرض مكونك، سيعتقد useEffect “حسنًا، لقد عملت بالفعل، لا يوجد شيء في المصفوفة، لذلك لن أضطر للعمل مرة أخرى. عد إلى النوم!”، وببساطة لا يفعل شيئًا. باختصار، مصفوفة فارغة [] تعني أن خطاف useEffect يعمل مرة واحدة عند تحميل المكون (on mount).
استخدام التأثيرات عند تغيير الأشياء (مكافئ componentDidUpdate)
لقد غطينا كيفية التأكد من أن خطاف useEffect يعمل مرة واحدة فقط، ولكن ماذا عن عندما يتلقى مكوننا خاصية جديدة؟ أو نريد تشغيل بعض التعليمات البرمجية عندما تتغير الحالة؟ تتيح لنا الخطافات القيام بذلك أيضًا!
useEffect(() => {
console.log("The name props has changed!")
}, [props.name]);
لاحظ كيف نمرر أشياء إلى مصفوفة useEffect هذه المرة، وهي props.name. في هذا السيناريو، سيعمل خطاف useEffect عند التحميل الأول كالمعتاد. كلما تلقى مكونك خاصية name جديدة من مكونه الأب، سيتم تشغيل خطاف useEffect، وستعمل التعليمات البرمجية بداخله.
يمكننا فعل الشيء نفسه مع متغيرات الحالة:
const [name, setName] = useState("Chris");
useEffect(() => {
console.log("The name state variable has changed!");
}, [name]);
كلما تغير متغير name، يعاد عرض المكون وسيعمل خطاف useEffect ويخرج الرسالة. نظرًا لأن هذه مصفوفة، يمكننا إضافة عناصر متعددة إليها:
const [name, setName] = useState("Chris");
useEffect(() => {
console.log("Something has changed!");
}, [name, props.name]);
هذه المرة، عندما يتغير متغير الحالة name، أو تتغير الخاصية name، سيعمل خطاف useEffect ويعرض رسالة وحدة التحكم.
هل يمكننا استخدام componentWillUnmount()؟
لتشغيل خطاف عندما يكون المكون على وشك إلغاء تحميله (unmount)، علينا فقط إرجاع دالة من خطاف useEffect:
useEffect(() => {
console.log('running effect');
return () => {
console.log('unmounting');
};
});
تُعرف هذه الدالة التي يتم إرجاعها باسم “دالة التنظيف” (cleanup function)، وهي تُشغل عندما يتم إلغاء تحميل المكون أو قبل تشغيل التأثير في المرة التالية (إذا تغيرت التبعيات).
هل يمكن استخدام خطافات مختلفة معًا؟
نعم! يمكنك استخدام أي عدد تريده من الخطافات في المكون الواحد، ومزجها ومطابقتها كما يحلو لك. هذا يوفر مرونة كبيرة في بناء المكونات المعقدة:
function App() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
useEffect(() => {
console.log("Component's name or age has changed!");
}, [name, age]);
return (
<div>
<h1>Hello, {name}!</h1>
<p>You are {age} years old.</p>
{/* Add input fields to change name and age */}
</div>
);
}
في هذا المثال، نستخدم useState مرتين لإدارة حالتين منفصلتين (الاسم والعمر)، ثم نستخدم useEffect لمراقبة التغييرات في كلتا الحالتين وتشغيل تأثير معين بناءً على ذلك.
الخلاصة: وماذا بعد؟
ها قد وصلنا إلى نهاية رحلتنا مع خطافات React. لقد رأينا كيف تتيح لنا الخطافات استخدام دوال JavaScript العادية لإنشاء مكونات React أبسط وأكثر وضوحًا، وتقليل كمية كبيرة من التعليمات البرمجية المكررة (boilerplate). إنها تمثل نقلة نوعية في طريقة بناء تطبيقات React، مما يجعل عملية التطوير أكثر متعة وكفاءة.
الآن، انطلق في عالم خطافات React الواسع وحاول بناء تطبيقاتك الخاصة! لا شيء يضاهي التعلم بالممارسة.
الخلاصة التقنية
تمثل خطافات React (React Hooks) ثورة حقيقية في تطوير المكونات الوظيفية، حيث ساهمت في سد الفجوة بين قدرات المكونات الوظيفية والمكونات المبنية على الفئات. قبل الخطافات، كان المطورون يواجهون تحديات كبيرة في إدارة الحالة والتأثيرات الجانبية ضمن المكونات الوظيفية، مما أجبرهم في كثير من الأحيان على اللجوء إلى المكونات المبنية على الفئات الأكثر تعقيدًا.
بفضل useState، أصبح بإمكاننا الآن إضافة حالة داخل المكونات الوظيفية بسهولة ومرونة، متجاوزين تعقيدات this.state و this.setState. أما useEffect، فقد قدم حلًا أنيقًا للتعامل مع التأثيرات الجانبية، مثل جلب البيانات أو التفاعل مع DOM، بطريقة تدمج وظائف دورة حياة المكونات المبنية على الفئات (مثل componentDidMount و componentDidUpdate و componentWillUnmount) في واجهة برمجية واحدة وموحدة. هذا التبسيط لا يؤدي فقط إلى تعليمات برمجية أنظف وأكثر قابلية للقراءة، بل يعزز أيضًا قابلية إعادة استخدام المنطق بين المكونات.
إن تبني الخطافات ليس مجرد خيار، بل أصبح معيارًا في تطوير React الحديث، مما يفتح آفاقًا جديدة لكتابة تطبيقات قوية وفعالة.