رفع الحالة ومشاركتها: Context
كيف تتشارك المكوّنات البعيدة البيانات نفسها؟ ⏱ نحو 30 دقيقة.
- ترفع الحالة إلى الأب المشترك وتمرر دوال التغيير للأبناء.
- تشرح تدفق البيانات في اتجاه واحد.
- تحل مشكلة حفر الخصائص بـ
Context. - تبني مزوّدًا بقيمة ودوال مع
Hookمخصص.
١حين يحتاج مكوّنان الحالة نفسها
في متجر: قائمة المنتجات فيها زر «أضف للسلة»، وفي الرأس عدّاد السلة. كلاهما يحتاج السلة نفسها. لو وضعنا الحالة في أحدهما لما رآها الآخر، لأن البيانات تتدفق من الأب إلى الابن فقط.
الحل اسمه رفع الحالة Lifting state up: ننقل الحالة إلى أقرب أب مشترك، ثم نمرر القيمة للأبناء خصائص، ونمرر دالة التغيير لمن يحتاج أن يغيّرها:
import { useState } from 'react';import Header from './Header.jsx';import ProductList from './ProductList.jsx'; export default function App() { const [cart, setCart] = useState([]); // الحالة في الأب المشترك function addToCart(product) { setCart([...cart, product]); } return ( <> <Header count={cart.length} /> <ProductList onAdd={addToCart} /> </> );}
export default function Header({ count }) { return <header><b>☕ متجر الواحة</b> · 🛒 السلة: {count}</header>;}
const products = ['قهوة عربية', 'تمر سكري', 'عسل سدر']; export default function ProductList({ onAdd }) { return ( <ul> {products.map(p => ( <li key={p}>{p} <button onClick={() => onAdd(p)}>أضف</button></li> ))} </ul> );}
الابن ProductList لا يعرف كيف تُحفظ السلة؛ هو فقط يستدعي onAdd التي وصلته. هكذا «يرسل» الابن حدثًا إلى أعلى، وتبقى الحالة في مكان واحد. هذا هو تدفق البيانات في اتجاه واحد: البيانات تنزل خصائص، والأحداث تصعد دوالًّا.
٢مشكلة «حفر الخصائص»
حين تكبر الشجرة، قد تحتاج قيمة في مكوّن بعيد جدًّا (مثل المستخدم الحالي، أو اللغة، أو الوضع الداكن)، فنمررها عبر مكوّنات وسيطة لا تستخدمها أصلًا. هذا اسمه حفر الخصائص Prop drilling:
<App user={user}> <Layout user={user}> // لا يستخدمه، يمرّره فقط <Sidebar user={user}> // لا يستخدمه، يمرّره فقط <Avatar user={user} /> // أخيرًا يستخدمه!
والحل في React: السياق Context. نضع القيمة في «مزوّد» Provider أعلى الشجرة، فيقرؤها أي مكوّن تحته مباشرة بـ useContext، دون المرور بالوسطاء.
٣Context خطوة بخطوة
- أنشئ السياق:
const ThemeContext = createContext('light') - غلّف الشجرة بالمزوّد وأعطه القيمة:
<ThemeContext.Provider value={{theme}}> - اقرأ القيمة في أي مكوّن تحته:
const theme = useContext(ThemeContext)
import { useState } from 'react';import { ThemeContext } from './theme.js';import Page from './Page.jsx'; export default function App() { const [theme, setTheme] = useState('light'); return ( <ThemeContext.Provider value={theme}> <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> {theme === 'light' ? '🌙 الوضع الداكن' : '☀️ الوضع الفاتح'} </button> <Page /> </ThemeContext.Provider> );}
import { createContext } from 'react'; export const ThemeContext = createContext('light');
import Card from './Card.jsx'; // Page لا يعرف شيئًا عن الوضع، ولا يمرّرهexport default function Page() { return ( <main> <Card title="الدورات" /> <Card title="الإعلانات" /> </main> );}
import { useContext } from 'react';import { ThemeContext } from './theme.js'; export default function Card({ title }) { const theme = useContext(ThemeContext); const dark = theme === 'dark'; return ( <div style={{ background: dark ? '#1f2933' : '#f4f7fa', color: dark ? '#e6edf3' : '#111', padding: 10, borderRadius: 10, marginTop: 8 }}> {title} ({theme}) </div> );}
٤سياق بقيمة ودوال: مزوّد السلة
نمط شائع جدًّا: مكوّن «مزوّد» يملك الحالة ودوالها، ويضعها كلها في السياق، ومعه Hook صغير يسهّل القراءة. بعدها أي مكوّن يقرأ السلة أو يضيف إليها بسطر واحد:
import { CartProvider } from './CartContext.jsx';import Header from './Header.jsx';import Products from './Products.jsx'; export default function App() { return ( <CartProvider> <Header /> <Products /> </CartProvider> );}
import { createContext, useContext, useState } from 'react'; const CartContext = createContext(null); export function CartProvider({ children }) { const [items, setItems] = useState([]); const add = (name, price) => setItems([...items, { name, price }]); const clear = () => setItems([]); const total = items.reduce((s, x) => s + x.price, 0); return ( <CartContext.Provider value={{ items, add, clear, total }}> {children} </CartContext.Provider> );} export function useCart() { return useContext(CartContext);}
import { useCart } from './CartContext.jsx'; export default function Header() { const { items, total, clear } = useCart(); return ( <header> 🛒 {items.length} منتجات · {total} ريال{' '} <button onClick={clear}>تفريغ</button> </header> );}
import { useCart } from './CartContext.jsx'; export default function Products() { const { add } = useCart(); return ( <p> <button onClick={() => add('قهوة', 35)}>قهوة 35</button>{' '} <button onClick={() => add('تمر', 45)}>تمر 45</button> </p> );}
| رفع الحالة والخصائص | Context | |
|---|---|---|
| المناسب | مكوّنان أو ثلاثة متقاربة | قيم يحتاجها كثيرون في أماكن بعيدة |
| أمثلة | حقل بحث وقائمة نتائج | المستخدم الحالي، واللغة، والوضع الداكن، والسلة |
| الوضوح | مصدر البيانات ظاهر في الخصائص | أخفى، فلا تستخدمه لكل شيء |
توقّع وتحقّق
مكوّنان شقيقان يحتاجان الحالة نفسها. أين نضعها؟
كيف يغيّر الابن حالة يملكها الأب؟
ما المشكلة التي يحلها Context؟
Prop drilling.لقراءة قيمة سياق داخل مكوّن نستخدم:
الخلاصة
- الحالة المشتركة في أقرب أب مشترك؛ القيم تنزل خصائص والأحداث تصعد دوالًّا.
- حفر الخصائص: تمرير قيمة عبر وسطاء لا يحتاجونها.
Context: createContext←Provider value←useContext.- نمط المزوّد: مكوّن يملك الحالة ودوالها +
HookمثلuseCart. - استخدم
Contextللقيم العامة فقط.
تحدٍّ برمجي: تبديل اللغة للتطبيق كله
متوسطتطبيق صغير بلغتين. زر في الأعلى يبدّل اللغة، ومكوّن بعيد يقرؤها من السياق.
- أنشئ
LangContextبقيمة افتراضية'ar'. - App يملك حالة اللغة، ويضع المزوّد، وفيه زر بالمعرّف
id="switch"يبدّل بين ar و en. - المكوّن
Welcome(داخلMainالذي لا يستقبل أي خاصية) يقرأ اللغة بـ useContext ويعرض<h1>: «مرحبًا» أو «Welcome». - لا تمرر اللغة خاصيةً إلى Main.
👀 اعرض الناتج المطلوب (وجرّبه)
- الكود يعمل بلا أخطاء في Console
- في البداية: مرحبًا
- الزر يبدّل إلى Welcome
- ضغطة أخرى تعيده إلى مرحبًا
- تستخدم createContext و useContext
- Main لا يستقبل خصائص
💡 تلميح 1
3: createContext ثم Provider ثم useContext.💡 تلميح 2
function Main() بلا معاملات.حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
import { useState, createContext, useContext } from 'react'; const LangContext = createContext('ar'); function Welcome() { const lang = useContext(LangContext); return <h1>{lang === 'ar' ? 'مرحبًا' : 'Welcome'}</h1>;} function Main() { return <main><Welcome /></main>;} export default function App() { const [lang, setLang] = useState('ar'); return ( <LangContext.Provider value={lang}> <button id="switch" onClick={() => setLang(lang === 'ar' ? 'en' : 'ar')}> {lang === 'ar' ? 'English' : 'العربية'} </button> <Main /> </LangContext.Provider> );}