React

Context API في React — إدارة الحالة العامة 2026

📅 2026-10-18⏱ 12 دقائق قراءة
في المقالات السابقة، تعلمت إدارة الحالة داخل مكوّنة واحدة. لكن ماذا لو احتجت **مشاركة البيانات** بين مكونات كثيرة؟ هنا يأتي دور **Context API**. في هذا الدليل العملي، سنأخذك خطوة بخطوة لتعلم Context API وكيفية استخدامه لإدارة الحالة العامة، مع تمارين وحلول. ## المشكلة: Props Drilling **Props Drilling** هو تمرير Props عبر **طبقات كثيرة** من المكونات. **مثال:** ```tsx // App → Layout → Header → UserMenu → Avatar function App() { const user = { name: "أحمد" }; return <Layout user={user} />; } function Layout({ user }) { return <Header user={user} />; } function Header({ user }) { return <UserMenu user={user} />; } function UserMenu({ user }) { return <Avatar user={user} />; } function Avatar({ user }) { return <img alt={user.name} />; } ``` **⚠️ المشاكل:** - **كود مكرر:** كل مكوّنة تمرر Props. - **صعب الصيانة:** تغيير بنية البيانات يؤثر على كل الطبقات. - **مرهق:** مكونات لا تحتاج البيانات، لكنها تمررها. ## الحل: Context API **Context** يسمح بمشاركة البيانات **مباشرة** مع أي مكوّنة تحتاجها، بدون Props Drilling. **تشبيه بسيط:** تخيل أن Props هي **تمرير ورقة من يد إلى يد**. Context هي **إعلان على لوحة** يقرأه كل من يحتاج. ## دورة عمل Context ``` 1. إنشاء Context (createContext) 2. توفير القيمة (Provider) 3. استخدام القيمة (useContext) ``` ## إنشاء Context ### 1. إنشاء Context جديد **أنشئ `src/contexts/UserContext.tsx`:** ```tsx import { createContext, useContext, useState, ReactNode } from "react"; interface User { name: string; email: string; } interface UserContextType { user: User | null; login: (user: User) => void; logout: () => void; } const UserContext = createContext<UserContextType | undefined>(undefined); export function UserProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState<User | null>(null); const login = (newUser: User) => setUser(newUser); const logout = () => setUser(null); return ( <UserContext.Provider value={{ user, login, logout }}> {children} </UserContext.Provider> ); } export function useUser() { const context = useContext(UserContext); if (!context) { throw new Error("useUser must be used within UserProvider"); } return context; } ``` **شرح:** - **`createContext`:** يُنشئ Context. - **`UserProvider`:** يُوفّر القيمة للمكونات. - **`useUser`:** Hook مخصص للاستخدام. ### 2. تغليف التطبيق **في `src/main.tsx`:** ```tsx import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; import { UserProvider } from "./contexts/UserContext"; import App from "./App"; import "./index.css"; createRoot(document.getElementById("root")!).render( <StrictMode> <BrowserRouter> <UserProvider> <App /> </UserProvider> </BrowserRouter> </StrictMode> ); ``` ### 3. استخدام Context ```tsx import { useUser } from "./contexts/UserContext"; function UserProfile() { const { user, login, logout } = useUser(); if (!user) { return ( <button onClick={() => login({ name: "أحمد", email: "ahmed@example.com" })}> تسجيل الدخول </button> ); } return ( <div> <p>مرحباً {user.name}</p> <button onClick={logout}>تسجيل الخروج</button> </div> ); } ``` **🎉 مبروك! لا مزيد من Props Drilling!** ## أمثلة عملية ### 1. Context للوضع الداكن **`src/contexts/ThemeContext.tsx`:** ```tsx import { createContext, useContext, useState, ReactNode } from "react"; type Theme = "light" | "dark"; interface ThemeContextType { theme: Theme; toggleTheme: () => void; } const ThemeContext = createContext<ThemeContextType | undefined>(undefined); export function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setTheme] = useState<Theme>("light"); const toggleTheme = () => { setTheme((prev) => (prev === "light" ? "dark" : "light")); }; return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ); } export function useTheme() { const context = useContext(ThemeContext); if (!context) { throw new Error("useTheme must be used within ThemeProvider"); } return context; } ``` **الاستخدام:** ```tsx function ThemeToggle() { const { theme, toggleTheme } = useTheme(); return ( <button onClick={toggleTheme}> {theme === "light" ? "🌙 داكن" : "☀️ فاتح"} </button> ); } ``` ### 2. Context للسلة (Cart) **`src/contexts/CartContext.tsx`:** ```tsx import { createContext, useContext, useState, ReactNode } from "react"; interface Product { id: number; name: string; price: number; } interface CartContextType { items: Product[]; addItem: (product: Product) => void; removeItem: (id: number) => void; clearCart: () => void; total: number; } const CartContext = createContext<CartContextType | undefined>(undefined); export function CartProvider({ children }: { children: ReactNode }) { const [items, setItems] = useState<Product[]>([]); const addItem = (product: Product) => { setItems((prev) => [...prev, product]); }; const removeItem = (id: number) => { setItems((prev) => prev.filter((item) => item.id !== id)); }; const clearCart = () => setItems([]); const total = items.reduce((sum, item) => sum + item.price, 0); return ( <CartContext.Provider value={{ items, addItem, removeItem, clearCart, total }} > {children} </CartContext.Provider> ); } export function useCart() { const context = useContext(CartContext); if (!context) { throw new Error("useCart must be used within CartProvider"); } return context; } ``` **الاستخدام:** ```tsx function ProductCard({ product }: { product: Product }) { const { addItem } = useCart(); return ( <div> <h3>{product.name}</h3> <p>{product.price} $</p> <button onClick={() => addItem(product)}>أضف للسلة</button> </div> ); } function CartSummary() { const { items, total, removeItem, clearCart } = useCart(); if (items.length === 0) return <p>السلة فارغة</p>; return ( <div> <h3>السلة ({items.length})</h3> <ul> {items.map((item) => ( <li key={item.id}> {item.name} - {item.price} $ <button onClick={() => removeItem(item.id)}>❌</button> </li> ))} </ul> <p>الإجمالي: {total} $</p> <button onClick={clearCart}>إفراغ السلة</button> </div> ); } ``` ### 3. Context للغة **`src/contexts/LanguageContext.tsx`:** ```tsx import { createContext, useContext, useState, ReactNode } from "react"; type Language = "ar" | "en"; const translations = { ar: { greeting: "مرحباً", goodbye: "إلى اللقاء", }, en: { greeting: "Hello", goodbye: "Goodbye", }, }; interface LanguageContextType { language: Language; toggleLanguage: () => void; t: (key: keyof typeof translations.ar) => string; } const LanguageContext = createContext<LanguageContextType | undefined>(undefined); export function LanguageProvider({ children }: { children: ReactNode }) { const [language, setLanguage] = useState<Language>("ar"); const toggleLanguage = () => { setLanguage((prev) => (prev === "ar" ? "en" : "ar")); }; const t = (key: keyof typeof translations.ar) => translations[language][key]; return ( <LanguageContext.Provider value={{ language, toggleLanguage, t }}> {children} </LanguageContext.Provider> ); } export function useLanguage() { const context = useContext(LanguageContext); if (!context) { throw new Error("useLanguage must be used within LanguageProvider"); } return context; } ``` ## Context مع TypeScript ### 1. النوع الكامل ```tsx interface MyContextType { value: string; setValue: (v: string) => void; } const MyContext = createContext<MyContextType | undefined>(undefined); ``` ### 2. التحقق من الوجود ```tsx export function useMyContext() { const context = useContext(MyContext); if (!context) { throw new Error("useMyContext must be used within MyProvider"); } return context; } ``` **الفائدة:** إذا استخدمت `useMyContext` خارج Provider، ستحصل على خطأ واضح. ## تحسين الأداء: فصل Contexts **⚠️ مشكلة:** تحديث قيمة Context يُعيد رسم **كل** المكونات المستهلكة. **الحل:** افصل Contexts حسب الاستخدام. ```tsx // بدلاً من: const AppContext = createContext({ user, theme, cart }); // استخدم: const UserContext = createContext(user); const ThemeContext = createContext(theme); const CartContext = createContext(cart); ``` **الفائدة:** كل مكوّنة تُعيد الرسم فقط عند تغيّر الـ Context الذي يستخدمه. ## useReducer مع Context للحالات المعقدة، استخدم `useReducer`: ```tsx import { createContext, useContext, useReducer, ReactNode } from "react"; type State = { count: number }; type Action = { type: "increment" } | { type: "decrement" } | { type: "reset" }; function reducer(state: State, action: Action): State { switch (action.type) { case "increment": return { count: state.count + 1 }; case "decrement": return { count: state.count - 1 }; case "reset": return { count: 0 }; } } const CountContext = createContext<{ state: State; dispatch: React.Dispatch<Action>; } | undefined>(undefined); export function CountProvider({ children }: { children: ReactNode }) { const [state, dispatch] = useReducer(reducer, { count: 0 }); return ( <CountContext.Provider value={{ state, dispatch }}> {children} </CountContext.Provider> ); } export function useCount() { const context = useContext(CountContext); if (!context) throw new Error("useCount must be used within CountProvider"); return context; } ``` **الاستخدام:** ```tsx function Counter() { const { state, dispatch } = useCount(); return ( <div> <p>{state.count}</p> <button onClick={() => dispatch({ type: "increment" })}>+</button> <button onClick={() => dispatch({ type: "decrement" })}>-</button> <button onClick={() => dispatch({ type: "reset" })}>إعادة</button> </div> ); } ``` ## Context vs Redux <table> <thead> <tr> <th>المعيار</th> <th>Context API</th> <th>Redux</th> </tr> </thead> <tbody> <tr> <td><strong>التعقيد</strong></td> <td>🟢 بسيط</td> <td>🟡 متوسط</td> </tr> <tr> <td><strong>الحجم</strong></td> <td>🪶 لا مكتبة</td> <td>📦 مكتبة خارجية</td> </tr> <tr> <td><strong>الأداء</strong></td> <td>✅ جيد</td> <td>✅ ممتاز</td> </tr> <tr> <td><strong>الأدوات</strong></td> <td>❌ محدودة</td> <td>✅ DevTools</td> </tr> <tr> <td><strong>الاستخدام</strong></td> <td>تطبيقات صغيرة/متوسطة</td> <td>تطبيقات كبيرة</td> </tr> </tbody> </table> **القاعدة:** - **Context:** للبيانات البسيطة (User, Theme, Language). - **Redux:** للحالة المعقدة (تطبيقات كبيرة). ## أفضل الممارسات ### 1. Context مخصص لكل نوع بيانات ```tsx // ✅ جيد: <UserProvider> <ThemeProvider> <LanguageProvider> <App /> </LanguageProvider> </ThemeProvider> </UserProvider> ``` ### 2. Hook مخصص لكل Context ```tsx export function useUser() { const context = useContext(UserContext); if (!context) throw new Error("useUser must be used within UserProvider"); return context; } ``` ### 3. تجنب Context للحالات المحلية **لا تستخدم Context** لحالة تستخدمها مكوّنة واحدة. استخدم `useState`. ## تمارين عملية ### تمرين 1: Context بسيط أنشئ Context لتخزين اسم المستخدم. **الحل:** ```tsx const NameContext = createContext<{ name: string; setName: (n: string) => void } | undefined>(undefined); export function NameProvider({ children }: { children: ReactNode }) { const [name, setName] = useState(""); return <NameContext.Provider value={{ name, setName }}>{children}</NameContext.Provider>; } export function useName() { const ctx = useContext(NameContext); if (!ctx) throw new Error("useName must be used within NameProvider"); return ctx; } ``` ### تمرين 2: Context للوضع الداكن أنشئ Context لتبديل الوضع الداكن. **الحل:** ```tsx type Theme = "light" | "dark"; const ThemeContext = createContext<{ theme: Theme; toggle: () => void } | undefined>(undefined); export function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setTheme] = useState<Theme>("light"); const toggle = () => setTheme((t) => (t === "light" ? "dark" : "light")); return <ThemeContext.Provider value={{ theme, toggle }}>{children}</ThemeContext.Provider>; } ``` ### تمرين 3: Context للعداد أنشئ Context لعداد مشترك بين مكونات. **الحل:** ```tsx const CountContext = createContext<{ count: number; increment: () => void } | undefined>(undefined); export function CountProvider({ children }: { children: ReactNode }) { const [count, setCount] = useState(0); const increment = () => setCount((c) => c + 1); return <CountContext.Provider value={{ count, increment }}>{children}</CountContext.Provider>; } ``` ### تمرين 4: Context للسلة أنشئ Context لإدارة سلة التسوق. **الحل:** ```tsx interface Product { id: number; name: string; price: number; } interface CartContextType { items: Product[]; add: (p: Product) => void; remove: (id: number) => void; total: number; } const CartContext = createContext<CartContextType | undefined>(undefined); export function CartProvider({ children }: { children: ReactNode }) { const [items, setItems] = useState<Product[]>([]); const add = (p: Product) => setItems((prev) => [...prev, p]); const remove = (id: number) => setItems((prev) => prev.filter((i) => i.id !== id)); const total = items.reduce((sum, i) => sum + i.price, 0); return ( <CartContext.Provider value={{ items, add, remove, total }}> {children} </CartContext.Provider> ); } ``` ### تمرين 5: Context للغة أنشئ Context للتبديل بين العربية والإنجليزية. **الحل:** ```tsx type Language = "ar" | "en"; const LanguageContext = createContext<{ language: Language; toggle: () => void } | undefined>(undefined); export function LanguageProvider({ children }: { children: ReactNode }) { const [language, setLanguage] = useState<Language>("ar"); const toggle = () => setLanguage((l) => (l === "ar" ? "en" : "ar")); return <LanguageContext.Provider value={{ language, toggle }}>{children}</LanguageContext.Provider>; } ``` ### تمرين 6: Context للإشعارات أنشئ Context لعرض الإشعارات. **الحل:** ```tsx interface Notification { id: number; message: string; } interface NotifContextType { notifications: Notification[]; addNotification: (msg: string) => void; } const NotifContext = createContext<NotifContextType | undefined>(undefined); export function NotifProvider({ children }: { children: ReactNode }) { const [notifications, setNotifications] = useState<Notification[]>([]); const addNotification = (message: string) => { const id = Date.now(); setNotifications((prev) => [...prev, { id, message }]); setTimeout(() => { setNotifications((prev) => prev.filter((n) => n.id !== id)); }, 3000); }; return ( <NotifContext.Provider value={{ notifications, addNotification }}> {children} <div className="notifications"> {notifications.map((n) => <div key={n.id}>{n.message}</div>)} </div> </NotifContext.Provider> ); } ``` ### تمرين 7: Context للمصادقة أنشئ Context لتسجيل الدخول والخروج. **الحل:** ```tsx interface User { name: string; email: string; } interface AuthContextType { user: User | null; login: (user: User) => void; logout: () => void; isAuthenticated: boolean; } const AuthContext = createContext<AuthContextType | undefined>(undefined); export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState<User | null>(null); const login = (u: User) => setUser(u); const logout = () => setUser(null); const isAuthenticated = !!user; return ( <AuthContext.Provider value={{ user, login, logout, isAuthenticated }}> {children} </AuthContext.Provider> ); } ``` ### تمرين 8: Context مع useReducer أنشئ Context لإدارة حالة معقدة بـ useReducer. **الحل:** ```tsx type State = { todos: { id: number; text: string }[] }; type Action = | { type: "add"; text: string } | { type: "remove"; id: number }; function reducer(state: State, action: Action): State { switch (action.type) { case "add": return { todos: [...state.todos, { id: Date.now(), text: action.text }] }; case "remove": return { todos: state.todos.filter((t) => t.id !== action.id) }; } } const TodoContext = createContext<{ state: State; dispatch: React.Dispatch<Action> } | undefined>(undefined); export function TodoProvider({ children }: { children: ReactNode }) { const [state, dispatch] = useReducer(reducer, { todos: [] }); return <TodoContext.Provider value={{ state, dispatch }}>{children}</TodoContext.Provider>; } ``` ## حل المشاكل الشائعة ### 🔴 المشكلة 1: `useContext must be used within Provider` **السبب:** استخدمت `useContext` خارج Provider. **الحل:** تأكد من تغليف التطبيق بـ `<Provider>`. ### 🔴 المشكلة 2: Context يُعيد الرسم كثيراً **السبب:** قيمة Context تتغير كثيراً. **الحل:** استخدم `useMemo` للقيمة: ```tsx const value = useMemo(() => ({ user, login, logout }), [user]); ``` ### 🔴 المشكلة 3: `Cannot read property 'xxx' of undefined` **السبب:** لم تُغلّف المكوّنة بـ Provider. **الحل:** ارفع الـ Provider لمستوى أعلى. ### 🔴 المشكلة 4: Context لا يتحدّث **السبب:** تعديل مباشر بدلاً من `setState`. **الحل:** استخدم `setState` الصحيح. ## جدول Context API <table> <thead> <tr> <th>الدالة</th> <th>الوظيفة</th> </tr> </thead> <tbody> <tr> <td><code>createContext</code></td> <td>إنشاء Context</td> </tr> <tr> <td><code>Provider</code></td> <td>توفير القيمة</td> </tr> <tr> <td><code>useContext</code></td> <td>استخدام القيمة</td> </tr> <tr> <td><code>useReducer</code></td> <td>إدارة حالة معقدة</td> </tr> </tbody> </table> ## قائمة تحقق نهائية <table> <thead> <tr> <th>المهمة</th> <th>الحالة</th> </tr> </thead> <tbody> <tr> <td>فهم Props Drilling</td> <td>⬜</td> </tr> <tr> <td>إنشاء Context</td> <td>⬜</td> </tr> <tr> <td>استخدام Provider</td> <td>⬜</td> </tr> <tr> <td>استخدام useContext</td> <td>⬜</td> </tr> <tr> <td>Context مع TypeScript</td> <td>⬜</td> </tr> <tr> <td>useReducer مع Context</td> <td>⬜</td> </tr> <tr> <td>حل التمارين الثمانية</td> <td>⬜</td> </tr> </tbody> </table> ## ماذا بعد هذا المقال؟ الآن بعد أن أتقنت Context API، أنت جاهز للمقال الأخير: 1. **مشروع متكامل** — تطبيق كامل يجمع كل ما تعلمته. ## الخلاصة في هذا المقال، تعلمت: - ✅ مشكلة Props Drilling. - ✅ إنشاء Context. - ✅ Provider و useContext. - ✅ Context مع TypeScript. - ✅ استخدام useReducer مع Context. - ✅ فصل Contexts للأداء. - ✅ Context vs Redux. - ✅ حل 8 تمارين عملية. **تذكر:** Context API أداة قوية، لكن **لا تبالغ في استخدامه**. أحياناً Props أبسط وأوضح. **هل أتقنت Context API؟ شاركنا في التعليقات!**