🎯 ما ستتعلمه في هذا الدرس
- useState — إدارة الحالة في Function Components
- useEffect — التعامل مع العمليات الجانبية (API, Timer, localStorage)
- useContext — مشاركة البيانات بين المكونات بدون Props Drilling
- useRef — الوصول المباشر لعناصر DOM والقيم الثابتة
- useMemo — تحسين الأداء بتخزين القيم المحسوبة
- useCallback — تحسين الأداء بتخزين الدوال
- useReducer — إدارة الحالة المعقدة في التطبيقات الكبيرة
📦 ما هو State ولماذا هو "قلب" React؟
State (الحالة) هي بيانات داخلية في المكون يمكن أن تتغير مع الوقت. بخلاف Props التي ثابتة ولا تتغير، State ديناميكي وقابل للتعديل. عندما تتغير State، React تعيد رسم المكون تلقائياً لتعكس التغييرات الجديدة.
في بداية مسيرتي كمطور، كنت أبني نموذج تسجيل دخول باستخدام JavaScript العادي. كلما كتب المستخدم في حقل، اضطررت لكتابة document.getElementById('input').value وتحديث العنصر يدوياً. مع 5 حقول، الكود أصبح فوضى! مع React و useState، كل حقل له State خاصة، وReact تحدث الواجهة تلقائياً.
متى نستخدم State؟
- نص المستخدم في حقل إدخال (Input)
- عدد العناصر في السلة
- حالة التحميل (Loading / Error / Success)
- البيانات المجلبة من API
- حالة الـ Modal (مفتوح/مغلق)
🎣 useState — أساس إدارة الحالة
useState هو أهم Hook في React. يسمح لك بإضافة state إلى Function Components. قبل Hooks (2019)، كان عليك استخدام Class Components لإدارة state — وهذا كان معقداً ويطول الكود. useState غير كل شيء.
الصيغة الأساسية
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div className="counter">
<h2>العدد الحالي: {count}</h2>
<div className="btn-group">
<button onClick={() => setCount(count - 1)}>−</button>
<button onClick={() => setCount(0)}>إعادة</button>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
</div>
);
}
⚠️ خطأ شائع 90% من المبتدئين: لا تكتب setCount(count + 1) مباشرة إذا كانت التحديث تعتمد على القيمة السابقة. استخدم دالة: setCount(prev => prev + 1). هذا يضمن القيمة الصحيحة حتى مع تحديثات سريعة.
useState مع Objects (الحالة المعقدة)
function UserProfile() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0,
isSubscribed: false
});
const updateField = (field, value) => {
setUser(prev => ({
...prev,
[field]: value
}));
};
return (
<form>
<input
value={user.name}
onChange={(e) => updateField('name', e.target.value)}
placeholder="الاسم"
/>
<input
type="email"
value={user.email}
onChange={(e) => updateField('email', e.target.value)}
placeholder="البريد"
/>
<p>مرحباً {user.name || 'زائر'}</p>
</form>
);
}
📝 useState مع Forms — التحكم الكامل في النماذج
من أكثر الاستخدامات شيوعاً لـ useState هو التعامل مع نماذج وحقول الإدخال. في مشروع لوحة تحكم (Dashboard) عملته، كان هناك نموذج إضافة منتج يحتوي على 12 حقل. بدون useState، كان الكود سيكون فوضى لا تصدق.
function ProductForm() {
const [product, setProduct] = useState({
name: '',
price: '',
category: '',
description: '',
inStock: true,
image: null
});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setProduct(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
const validate = () => {
const newErrors = {};
if (!product.name) newErrors.name = 'اسم المنتج مطلوب';
if (!product.price || product.price <= 0) newErrors.price = 'السعر يجب أن يكون أكبر من صفر';
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validate()) return;
setIsSubmitting(true);
try {
await fetch('/api/products', {
method: 'POST',
body: JSON.stringify(product)
});
alert('تم إضافة المنتج بنجاح!');
} catch (err) {
setErrors({ submit: 'حدث خطأ أثناء الحفظ' });
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={product.name}
onChange={handleChange}
placeholder="اسم المنتج"
/>
{errors.name && <span className="error">{errors.name}</span>}
<input
name="price"
type="number"
value={product.price}
onChange={handleChange}
placeholder="السعر"
/>
{errors.price && <span className="error">{errors.price}</span>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'جاري الحفظ...' : 'حفظ المنتج'}
</button>
{errors.submit && <p className="error">{errors.submit}</p>}
</form>
);
}
💡 نصيحة احترافية: في النماذج الكبيرة (10+ حقول)، استخدم مكتبة React Hook Form. تقلل الكود بنسبة 70% وتتعامل مع Validation وPerformance تلقائياً.
⏰ useEffect — التعامل مع العمليات الجانبية
useEffect يسمح لك بتنفيذ عمليات جانبية (Side Effects) في مكونك. العمليات الجانبية هي كل ما هو خارج نطاق رسم الواجهة: جلب البيانات من API، الاشتراك في أحداث، تحديث localStorage، تشغيل مؤقتات (Timers)، وغيرها.
في مشروع تطبيق طقس عملته، استخدمت useEffect لجلب بيانات الطقس من API عند فتح التطبيق. بدون useEffect، كان الطلب سيحدث في كل رسم (Render) — مما يسبب آلاف الطلبات غير الضرورية!
الصيغة الأساسية
import { useState, useEffect } from 'react';
function WeatherApp() {
const [weather, setWeather] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchWeather = async () => {
try {
const res = await fetch(
'https://api.weather.com/v1/current?city=Cairo'
);
const data = await res.json();
setWeather(data);
} catch (err) {
setError('تعذر جلب بيانات الطقس');
} finally {
setLoading(false);
}
};
fetchWeather();
}, []);
if (loading) return <div className="spinner">جاري التحميل...</div>;
if (error) return <div className="error">{error}</div>;
return (
<div className="weather-card">
<h2>{weather.city}</h2>
<p>🌡️ {weather.temperature}°C</p>
<p>💧 رطوبة: {weather.humidity}%</p>
</div>
);
}
Dependency Array — التحكم الدقيق في متى يعمل useEffect
المصفوفة الثالثة في useEffect (dependency array) هي الأكثر أهمية وأكثر ما يسبب أخطاء للمبتدئين. هناك 3 حالات:
| الصيغة |
متى يعمل؟ |
متى تستخدمه؟ |
useEffect(() => {...}) |
في كل Render (خطأ شائع!) |
❌ تجنب هذا دائماً |
useEffect(() => {...}, []) |
مرة واحدة عند التحميل |
✅ جلب البيانات الأولية، الاشتراكات |
useEffect(() => {...}, [userId]) |
عند تغيير userId فقط |
✅ جلب بيانات جديدة عند تغيير المعرف |
useEffect(() => {
setCount(count + 1);
});
useEffect(() => {
fetchData();
}, []);
useEffect(() => {
fetchUser(userId);
}, [userId]);
useEffect(() => {
calculateTotal(price, quantity);
}, [price, quantity]);
⚠️ خطأ قاتل: إذا نسيت dependency array واستخدمت setState داخل useEffect، سيحدث Loop لانهائي! React ترسم → useEffect يعمل → setState → React ترسم → تكرار. دائماً استخدم [] أو القيم الصحيحة.
Cleanup — تنظيف الموارد
أحياناً تحتاج لتنظيف الموارد عند إلغاء المكون (Unmount). مثلاً: إلغاء الاشتراكات، إيقاف المؤقتات، إغلاق اتصالات WebSocket. يمكنك إرجاع دالة من useEffect تعمل كـ Cleanup.
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = new WebSocket(`wss://chat.com/room/${roomId}`);
connection.onmessage = (event) => {
setMessages(prev => [...prev, JSON.parse(event.data)]);
};
return () => {
connection.close();
console.log('تم إغلاق الاتصال بغرفة', roomId);
};
}, [roomId]);
return (
<div>
<h3>غرفة: {roomId}</h3>
{messages.map(msg => (
<p key={msg.id}>{msg.text}</p>
))}
</div>
);
}
🌐 useContext — مشاركة البيانات بدون Props Drilling
Props Drilling هو تمرير Props عبر 5-6 مستويات من المكونات. في تطبيق حقيقي (مثل تطبيق تواصل اجتماعي)، بيانات المستخدم (user) تحتاج أن تصل إلى Header وSidebar وContent وFooter — كلها في مستويات مختلفة. useContext يحل هذه المشكلة.
import { createContext, useContext, useState } from 'react';
const UserContext = createContext(null);
function App() {
const [user, setUser] = useState({
id: 1,
name: 'أحمد',
email: 'ahmed@example.com',
role: 'admin'
});
return (
<UserContext.Provider value={{ user, setUser }}>
<Dashboard />
</UserContext.Provider>
);
}
function Header() {
const { user } = useContext(UserContext);
return (
<header>
<span>مرحباً {user.name}</span>
<span className="badge">{user.role}</span>
</header>
);
}
function ProfilePage() {
const { user, setUser } = useContext(UserContext);
const updateName = (newName) => {
setUser(prev => ({ ...prev, name: newName }));
};
return (
<div>
<h2>الملف الشخصي</h2>
<p>الاسم: {user.name}</p>
<p>البريد: {user.email}</p>
</div>
);
}
💡 نصيحة: useContext رائع للبيانات "العالمية" (User, Theme, Language). لكن لا تستخدمه لكل شيء — Props لا تزال أفضل للبيانات المحلية بين Parent و Child المباشر.
📌 useRef — الوصول المباشر لعناصر DOM
useRef يسمح بالوصول المباشر لعناصر DOM (مثل document.getElementById) دون إعادة رسم المكون. كما يُستخدم لتخزين قيم ثابتة (Persisted Values) بين الرسومات دون تفعيل Re-render.
الاستخدام 1: التركيز على Input تلقائياً
function SearchBox() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return (
<input
ref={inputRef}
type="text"
placeholder="ابحث هنا..."
/>
);
}
الاستخدام 2: تخزين قيمة ثابتة (مثل عدد الرسومات)
function RenderCounter() {
const renderCount = useRef(0);
renderCount.current += 1;
return <p>عدد الرسومات: {renderCount.current}</p>;
}
الاستخدام 3: تخزين Interval ID لتنظيفه لاحقاً
function Timer() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
const startTimer = () => {
if (intervalRef.current) return;
intervalRef.current = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
};
const stopTimer = () => {
clearInterval(intervalRef.current);
intervalRef.current = null;
};
useEffect(() => {
return () => stopTimer();
}, []);
return (
<div>
<p>⏱️ {seconds} ثانية</p>
<button onClick={startTimer}>بدء</button>
<button onClick={stopTimer}>إيقاف</button>
</div>
);
}
⚡ useMemo — تحسين الأداء بتخزين القيم المحسوبة
useMemo يخزن نتيجة حسابية مكلفة ويعيد حسابها فقط عند تغيير dependencies. في مشروع تحليل بيانات عملته، كان هناك حساب يستغرق 200ms — استخدام useMemo قلل هذا إلى 2ms في 95% من الرسومات.
function ProductList({ products, filter }) {
const filtered = useMemo(() => {
console.log('إعادة حساب الفلترة...');
return products
.filter(p => p.price >= filter.minPrice)
.sort((a, b) => a.price - b.price);
}, [products, filter.minPrice]);
return (
<ul>
{filtered.map(product => (
<li key={product.id}>
{product.name} — {product.price} جنيه
</li>
))}
</ul>
);
}
🎯 متى تستخدم useMemo؟ فقط عند الحسابات المكلفة (فلترة 1000+ عنصر، تحويل بيانات معقد). لا تستخدمه في كل شيء — التخزين نفسه يكلف ذاكرة!
🔒 useCallback — تحسين الأداء بتخزين الدوال
useCallback يشبه useMemo لكنه يخزن دالة (Function) بدلاً من قيمة. مفيد عند تمرير دوال كـ Props لمكونات ابنة — يمنع إعادة إنشاء الدالة في كل Render، مما يمنع Re-render غير ضروري للمكونات الابنة.
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
const handleNameChange = useCallback((newName) => {
setName(newName);
}, []);
return (
<div>
<p>العدد: {count}</p>
<ChildButton onClick={handleClick} label="زيادة" />
<NameInput value={name} onChange={handleNameChange} />
</div>
);
}
const ChildButton = React.memo(({ onClick, label }) => {
console.log('ChildButton يُرسم');
return <button onClick={onClick}>{label}</button>;
});
⚠️ تحذير: لا تستخدم useCallback في كل مكان! في 90% من الحالات، الأداء الطبيعي كافٍ. استخدمه فقط عند:
- تمرير دوال كـ Props لمكونات كبيرة (React.memo)
- دوال تُستخدم كـ Dependencies في useEffect
- مكتبات خارجية تتطلب استقرار References (مثل React Hook Form)
🗂️ useReducer — إدارة الحالة المعقدة
useReducer هو Hook بديل لـ useState عندما تكون الحالة معقدة أو تحتوي على منطق متعدد. مستوحى من Redux، يستخدم "Reducer Function" لتحديث الحالة. في تطبيق سلة مشتريات (Shopping Cart) عملته، useReducer جعل الكود أنظف بكثير.
const ACTIONS = {
ADD_ITEM: 'ADD_ITEM',
REMOVE_ITEM: 'REMOVE_ITEM',
UPDATE_QUANTITY: 'UPDATE_QUANTITY',
CLEAR_CART: 'CLEAR_CART'
};
function cartReducer(state, action) {
switch (action.type) {
case ACTIONS.ADD_ITEM:
const existingItem = state.items.find(i => i.id === action.payload.id);
if (existingItem) {
return {
...state,
items: state.items.map(i =>
i.id === action.payload.id
? { ...i, quantity: i.quantity + 1 }
: i
),
total: state.total + action.payload.price
};
}
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }],
total: state.total + action.payload.price
};
case ACTIONS.REMOVE_ITEM:
const item = state.items.find(i => i.id === action.payload);
return {
...state,
items: state.items.filter(i => i.id !== action.payload),
total: state.total - (item.price * item.quantity)
};
case ACTIONS.UPDATE_QUANTITY:
return {
...state,
items: state.items.map(i =>
i.id === action.payload.id
? { ...i, quantity: action.payload.quantity }
: i
),
total: state.items.reduce((sum, i) =>
i.id === action.payload.id
? sum + (action.payload.quantity * i.price)
: sum + (i.quantity * i.price),
0
)
};
case ACTIONS.CLEAR_CART:
return { items: [], total: 0 };
default:
return state;
}
}
function ShoppingCart() {
const [cart, dispatch] = useReducer(cartReducer, {
items: [],
total: 0
});
const addItem = (product) => {
dispatch({ type: ACTIONS.ADD_ITEM, payload: product });
};
const removeItem = (id) => {
dispatch({ type: ACTIONS.REMOVE_ITEM, payload: id });
};
return (
<div>
<h2>🛒 السلة ({cart.items.length})</h2>
<p>الإجمالي: {cart.total} جنيه</p>
{cart.items.map(item => (
<div key={item.id}>
{item.name} × {item.quantity}
<button onClick={() => removeItem(item.id)}>❌</button>
</div>
))}
</div>
);
}
🎯 متى تستخدم useReducer بدلاً من useState؟
- الحالة تحتوي على كائن معقد (nested objects)
- منطق التحديث معقد (أكثر من 3-4 حالات)
- الحالة التالية تعتمد على الحالة السابقة
- تريد تقليل Re-renders غير الضرورية
📊 مقارنة سريعة: متى تستخدم كل Hook؟
| Hook |
الاستخدام |
مثال واقعي |
useState |
حالة بسيطة (عداد، نص، boolean) |
عداد السلة، نص البحث |
useEffect |
عمليات جانبية (API, Timer) |
جلب المنتجات، مؤقت |
useContext |
بيانات مشتركة (User, Theme) |
بيانات المستخدم، الوضع الليلي |
useRef |
DOM مباشر، قيم ثابتة |
تركيز Input، عداد الرسومات |
useMemo |
تخزين حسابات مكلفة |
فلترة 1000 منتج، تحويل بيانات |
useCallback |
تخزين دوال |
دوال تُمرر لـ Child Components |
useReducer |
حالة معقدة متعددة الحالات |
سلة مشتريات، نموذج متعدد الخطوات |
⚠️ قواعد ذهبية لاستخدام Hooks
- تُستخدم فقط في أعلى Function Component: لا تضع Hooks داخل if أو loop أو دالة متداخلة
- تُستخدم فقط في Function Components أو Custom Hooks: لا يمكن استخدامها في Class Components
- أسماء مخصصة: كل Hook يبدأ بـ use (useState, useEffect, useContext)
- ترتيب ثابت: يجب استدعاء Hooks بنفس الترتيب في كل Render
- لا تستخدم Hooks بشكل مشروط:
if (condition) useState() ❌
⚠️ خطأ قاتل: إذا استخدمت Hook داخل if أو for، React ستفقد التتبع وقد يتعطل التطبيق! ESLint Plugin (eslint-plugin-react-hooks) يكتشف هذا تلقائياً — ثبته في مشاريعك.
🚀 تمرين عملي: بناء تطبيق مهام (To-Do App) كامل
قبل الانتقال للدرس التالي، جرب بناء تطبيق مهام كامل يستخدم 5 Hooks مختلفة:
📝 تلميح: ابدأ بـ useState فقط. ثم أضف useEffect للـ localStorage. ثم useRef للتركيز. ثم useMemo للفلترة. لا تحاول كل شيء دفعة واحدة — اختبر كل Hook على حدة!
📚 الخلاصة والخطوات التالية
في هذا الدرس تعلمت 7 Hooks أساسية:
- ✅ useState — إدارة الحالة البسيطة (عداد، نص، boolean)
- ✅ useEffect — العمليات الجانبية (API, Timer, localStorage) مع Dependency Array
- ✅ useContext — مشاركة البيانات بين المكونات بدون Props Drilling
- ✅ useRef — الوصول المباشر لـ DOM وتخزين قيم ثابتة
- ✅ useMemo — تحسين الأداء بتخزين الحسابات المكلفة
- ✅ useCallback — تحسين الأداء بتخزين الدوال
- ✅ useReducer — إدارة الحالة المعقدة (سلة مشتريات، نماذج متعددة الخطوات)
الدرس القادم: سنتعلم React Router — التنقل بين الصفحات في تطبيق React Single Page Application (SPA). سنبني تطبيق متعدد الصفحات مع Navbar ديناميكي وحماية Routes!
🎯 مهمة قبل الدرس القادم: أكمل تمرين To-Do App. جرب استخدام useReducer بدلاً من useState لإدارة المهام. شارك الكود على واتساب 01229068356 وسنراجعه معك!