← السابق المستوى 4 - الدرس 3 من 12

🎣 State و Hooks الكاملة: إدارة الحالة والأداء في React

دليلك الشامل لـ 7 Hooks أساسية مع أمثلة عملية من مشاريع حقيقية

تقدمك في المستوى الرابع 25%

🎯 ما ستتعلمه في هذا الدرس

  • 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؟

🎣 useState — أساس إدارة الحالة

useState هو أهم Hook في React. يسمح لك بإضافة state إلى Function Components. قبل Hooks (2019)، كان عليك استخدام Class Components لإدارة state — وهذا كان معقداً ويطول الكود. useState غير كل شيء.

الصيغة الأساسية

// const [القيمة, دالة التحديث] = 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 فقط ✅ جلب بيانات جديدة عند تغيير المعرف
// ❌ سيء: يعمل في كل Render — يسبب Loop لانهائي! useEffect(() => { setCount(count + 1); }); // ✅ جيد: يعمل مرة واحدة useEffect(() => { fetchData(); }, []); // ✅ جيد: يعمل عند تغيير userId 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]); // يعمل Cleanup عند تغيير 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 يحل هذه المشكلة.

// 1. إنشاء Context import { createContext, useContext, useState } from 'react'; const UserContext = createContext(null); // 2. Provider — يغلف التطبيق ويوفر البيانات function App() { const [user, setUser] = useState({ id: 1, name: 'أحمد', email: 'ahmed@example.com', role: 'admin' }); return ( <UserContext.Provider value={{ user, setUser }}> <Dashboard /> // لا حاجة لتمرير user! </UserContext.Provider> ); } // 3. استخدام Context في أي مكون داخل 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(() => { // التركيز على Input عند فتح الصفحة inputRef.current.focus(); }, []); return ( <input ref={inputRef} type="text" placeholder="ابحث هنا..." /> ); }

الاستخدام 2: تخزين قيمة ثابتة (مثل عدد الرسومات)

function RenderCounter() { const renderCount = useRef(0); // useRef لا يُسبب Re-render عند التغيير 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 }) { // ❌ سيء: يعيد الحساب في كل Render حتى لو لم تتغير البيانات // const filtered = products.filter(p => p.price > filter.minPrice); // ✅ جيد: يعيد الحساب فقط عند تغيير 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(''); // ❌ سيء: تُنشأ دالة جديدة في كل Render — Child يُعاد رسمه دائماً // const handleClick = () => setCount(c => c + 1); // ✅ جيد: الدالة تُنشأ مرة واحدة فقط (أو عند تغيير dependencies) const handleClick = useCallback(() => { setCount(c => c + 1); }, []); // لا dependencies = تُنشأ مرة واحدة // ✅ جيد: تُنشأ فقط عند تغيير name const handleNameChange = useCallback((newName) => { setName(newName); }, []); return ( <div> <p>العدد: {count}</p> // Child لن يُعاد رسمه إذا لم تتغير Props <ChildButton onClick={handleClick} label="زيادة" /> <NameInput value={name} onChange={handleNameChange} /> </div> ); } // ChildButton.js — يستخدم React.memo لتجنب Re-render غير ضروري 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 جعل الكود أنظف بكثير.

// 1. تعريف Actions (الأحداث الممكنة) const ACTIONS = { ADD_ITEM: 'ADD_ITEM', REMOVE_ITEM: 'REMOVE_ITEM', UPDATE_QUANTITY: 'UPDATE_QUANTITY', CLEAR_CART: 'CLEAR_CART' }; // 2. Reducer Function — تحديث الحالة بناءً على Action 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; } } // 3. استخدام useReducer في المكون 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

⚠️ خطأ قاتل: إذا استخدمت Hook داخل if أو for، React ستفقد التتبع وقد يتعطل التطبيق! ESLint Plugin (eslint-plugin-react-hooks) يكتشف هذا تلقائياً — ثبته في مشاريعك.

🚀 تمرين عملي: بناء تطبيق مهام (To-Do App) كامل

قبل الانتقال للدرس التالي، جرب بناء تطبيق مهام كامل يستخدم 5 Hooks مختلفة:

// التحدي: أنشئ تطبيق To-Do App يستخدم: // 1. useState — لحالة المهام (tasks) والنص الجديد // 2. useEffect — لحفظ المهام في localStorage واسترجاعها // 3. useRef — للتركيز على Input تلقائياً بعد إضافة مهمة // 4. useMemo — لتصفية المهام المكتملة/النشطة // 5. useCallback — لتجنب إعادة إنشاء دوال الحذف/التعديل // المتطلبات: // - إضافة مهمة جديدة // - تحديد مهمة كمكتملة (toggle) // - حذف مهمة // - فلترة: الكل / النشطة / المكتملة // - حفظ تلقائي في localStorage // - عداد المهام النشطة في الـ Header
📝 تلميح: ابدأ بـ useState فقط. ثم أضف useEffect للـ localStorage. ثم useRef للتركيز. ثم useMemo للفلترة. لا تحاول كل شيء دفعة واحدة — اختبر كل Hook على حدة!

📚 الخلاصة والخطوات التالية

في هذا الدرس تعلمت 7 Hooks أساسية:

الدرس القادم: سنتعلم React Router — التنقل بين الصفحات في تطبيق React Single Page Application (SPA). سنبني تطبيق متعدد الصفحات مع Navbar ديناميكي وحماية Routes!

🎯 مهمة قبل الدرس القادم: أكمل تمرين To-Do App. جرب استخدام useReducer بدلاً من useState لإدارة المهام. شارك الكود على واتساب 01229068356 وسنراجعه معك!