أساسيات React.js

useContext و Context API - إدارة الحالة العامة

18 دقيقة الدرس 13 من 40

فهم السياق في React

يوفر السياق طريقة لتمرير البيانات عبر شجرة المكونات دون الحاجة إلى تمرير الخصائص يدويًا على كل مستوى. تم تصميمه لمشاركة البيانات التي يمكن اعتبارها "عامة" لشجرة من مكونات React، مثل المستخدم المصادق عليه الحالي، أو المظهر، أو اللغة المفضلة.

يتكون Context API من جزأين رئيسيين: مكون Provider الذي يوفر البيانات، وطريقة لاستهلاك تلك البيانات في المكونات الفرعية باستخدام إما useContext hook أو مكون Consumer.

متى تستخدم السياق

استخدم السياق عندما يكون لديك بيانات تحتاج إلى أن تكون قابلة للوصول من قبل العديد من المكونات على مستويات تداخل مختلفة. حالات الاستخدام الشائعة تشمل: السمات، مصادقة المستخدم، تفضيلات اللغة، وإعدادات التطبيق.

إنشاء سياق

أولاً، أنشئ سياقًا باستخدام React.createContext():

import React, { createContext, useState } from 'react';

// إنشاء سياق بقيمة افتراضية
const ThemeContext = createContext('light');

export default ThemeContext;

توفير قيمة السياق

لف شجرة المكونات الخاصة بك مع Provider لجعل قيمة السياق متاحة لجميع المكونات الفرعية:

import React, { useState } from 'react';
import ThemeContext from './ThemeContext';

function App() {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <div className={`app ${theme}`}>
        <Header />
        <MainContent />
        <Footer />
      </div>
    </ThemeContext.Provider>
  );
}

export default App;

استهلاك السياق مع useContext

useContext hook هو أسهل طريقة للوصول إلى قيم السياق في مكونات الدوال:

import React, { useContext } from 'react';
import ThemeContext from './ThemeContext';

function Header() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <header className={`header ${theme}`}>
      <h1>تطبيقي</h1>
      <button onClick={toggleTheme}>
        التبديل إلى الوضع {theme === 'light' ? 'الداكن' : 'الفاتح'}
      </button>
    </header>
  );
}

export default Header;

نمط موفر السياق

أنشئ مكون موفر مخصص يغلف إنشاء السياق، وإدارة الحالة، ومنطق المزود. هذا يحافظ على الكود منظمًا ويجعل السياق أسهل في الاستخدام.

مثال كامل لسياق المظهر

إليك مثال كامل مع موفر مخصص:

// ThemeContext.js
import React, { createContext, useState, useContext } from 'react';

const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  const value = {
    theme,
    toggleTheme
  };

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

// Hook مخصص لاستخدام سياق المظهر
export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

// App.js
import React from 'react';
import { ThemeProvider } from './ThemeContext';
import Header from './Header';
import Content from './Content';

function App() {
  return (
    <ThemeProvider>
      <div className="app">
        <Header />
        <Content />
      </div>
    </ThemeProvider>
  );
}

export default App;

// Header.js
import React from 'react';
import { useTheme } from './ThemeContext';

function Header() {
  const { theme, toggleTheme } = useTheme();

  return (
    <header style={{
      background: theme === 'light' ? '#fff' : '#333',
      color: theme === 'light' ? '#000' : '#fff'
    }}>
      <h1>مثال على المظهر</h1>
      <button onClick={toggleTheme}>
        تبديل المظهر
      </button>
    </header>
  );
}

export default Header;

مثال سياق المصادقة

حالة استخدام شائعة هي إدارة حالة مصادقة المستخدم:

// AuthContext.js
import React, { createContext, useState, useContext } from 'react';

const AuthContext = createContext();

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);

  const login = async (email, password) => {
    setLoading(true);
    try {
      // محاكاة استدعاء API
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ email, password }),
        headers: { 'Content-Type': 'application/json' }
      });
      const data = await response.json();
      setUser(data.user);
      localStorage.setItem('token', data.token);
    } catch (error) {
      console.error('فشل تسجيل الدخول:', error);
      throw error;
    } finally {
      setLoading(false);
    }
  };

  const logout = () => {
    setUser(null);
    localStorage.removeItem('token');
  };

  const value = {
    user,
    loading,
    login,
    logout,
    isAuthenticated: !!user
  };

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}

// الاستخدام في المكون
import React from 'react';
import { useAuth } from './AuthContext';

function UserProfile() {
  const { user, logout, isAuthenticated } = useAuth();

  if (!isAuthenticated) {
    return <div>يرجى تسجيل الدخول</div>;
  }

  return (
    <div>
      <h2>مرحبًا، {user.name}!</h2>
      <p>البريد الإلكتروني: {user.email}</p>
      <button onClick={logout}>تسجيل الخروج</button>
    </div>
  );
}

اعتبارات أداء السياق

عندما تتغير قيمة السياق، ستتم إعادة عرض جميع المكونات التي تستهلك هذا السياق. لتحسين الأداء، قم بتقسيم السياقات حسب الاهتمام واستخدم React.memo للمكونات الفرعية المكلفة.

سياقات متعددة

يمكنك تداخل موفري سياق متعددين لاستخدام سياقات مختلفة:

import React from 'react';
import { ThemeProvider } from './ThemeContext';
import { AuthProvider } from './AuthContext';
import { LanguageProvider } from './LanguageContext';

function App() {
  return (
    <AuthProvider>
      <ThemeProvider>
        <LanguageProvider>
          <MainApp />
        </LanguageProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

// مكون يستخدم سياقات متعددة
function Dashboard() {
  const { user } = useAuth();
  const { theme } = useTheme();
  const { language, t } = useLanguage();

  return (
    <div className={`dashboard ${theme}`}>
      <h1>{t('welcome')}، {user.name}!</h1>
      <p>اللغة الحالية: {language}</p>
    </div>
  );
}

تحسين تحديثات السياق

قم بتقسيم قيم السياق لمنع إعادة العرض غير الضرورية:

// بدلاً من سياق واحد بجميع بيانات المستخدم
// قسم إلى سياقات منفصلة للحالة والإجراءات

// UserStateContext.js
const UserStateContext = createContext();
const UserActionsContext = createContext();

export function UserProvider({ children }) {
  const [user, setUser] = useState(null);

  // الإجراءات لا تتغير، لذا لن تتسبب في إعادة العرض
  const actions = useMemo(() => ({
    updateUser: (newUser) => setUser(newUser),
    clearUser: () => setUser(null)
  }), []);

  return (
    <UserStateContext.Provider value={user}>
      <UserActionsContext.Provider value={actions}>
        {children}
      </UserActionsContext.Provider>
    </UserStateContext.Provider>
  );
}

export function useUserState() {
  return useContext(UserStateContext);
}

export function useUserActions() {
  return useContext(UserActionsContext);
}

// المكونات تعيد العرض فقط عندما يتغير سياقها المحدد
function UserDisplay() {
  const user = useUserState(); // إعادة العرض عندما يتغير المستخدم
  return <div>{user?.name}</div>;
}

function UserControls() {
  const { updateUser } = useUserActions(); // لا تعيد العرض أبدًا
  return <button onClick={() => updateUser({ name: 'محمد' })}>تحديث</button>;
}

سياق اللغة/الترجمة

// LanguageContext.js
import React, { createContext, useState, useContext } from 'react';

const translations = {
  en: {
    welcome: 'Welcome',
    goodbye: 'Goodbye',
    hello: 'Hello'
  },
  ar: {
    welcome: 'مرحبا',
    goodbye: 'وداعا',
    hello: 'أهلا'
  }
};

const LanguageContext = createContext();

export function LanguageProvider({ children }) {
  const [language, setLanguage] = useState('en');

  const t = (key) => {
    return translations[language][key] || key;
  };

  const changeLanguage = (lang) => {
    setLanguage(lang);
    document.documentElement.lang = lang;
    document.documentElement.dir = lang === 'ar' ? 'rtl' : 'ltr';
  };

  const value = {
    language,
    changeLanguage,
    t
  };

  return (
    <LanguageContext.Provider value={value}>
      {children}
    </LanguageContext.Provider>
  );
}

export function useLanguage() {
  const context = useContext(LanguageContext);
  if (!context) {
    throw new Error('useLanguage must be used within LanguageProvider');
  }
  return context;
}

// الاستخدام
function Greeting() {
  const { t, language, changeLanguage } = useLanguage();

  return (
    <div>
      <h1>{t('hello')}</h1>
      <button onClick={() => changeLanguage('en')}>English</button>
      <button onClick={() => changeLanguage('ar')}>العربية</button>
    </div>
  );
}

مثال سياق عربة التسوق

// CartContext.js
import React, { createContext, useContext, useReducer } from 'react';

const CartContext = createContext();

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      const existingIndex = state.items.findIndex(
        item => item.id === action.payload.id
      );

      if (existingIndex >= 0) {
        const newItems = [...state.items];
        newItems[existingIndex].quantity += 1;
        return { ...state, items: newItems };
      }

      return {
        ...state,
        items: [...state.items, { ...action.payload, quantity: 1 }]
      };

    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload)
      };

    case 'UPDATE_QUANTITY':
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload.id
            ? { ...item, quantity: action.payload.quantity }
            : item
        )
      };

    case 'CLEAR_CART':
      return { ...state, items: [] };

    default:
      return state;
  }
}

export function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, { items: [] });

  const addItem = (item) => {
    dispatch({ type: 'ADD_ITEM', payload: item });
  };

  const removeItem = (id) => {
    dispatch({ type: 'REMOVE_ITEM', payload: id });
  };

  const updateQuantity = (id, quantity) => {
    dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } });
  };

  const clearCart = () => {
    dispatch({ type: 'CLEAR_CART' });
  };

  const total = state.items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );

  const value = {
    items: state.items,
    addItem,
    removeItem,
    updateQuantity,
    clearCart,
    total,
    itemCount: state.items.length
  };

  return (
    <CartContext.Provider value={value}>
      {children}
    </CartContext.Provider>
  );
}

export function useCart() {
  const context = useContext(CartContext);
  if (!context) {
    throw new Error('useCart must be used within CartProvider');
  }
  return context;
}

// الاستخدام في المكون
function CartSummary() {
  const { items, total, itemCount, removeItem } = useCart();

  return (
    <div>
      <h2>عربة التسوق ({itemCount} عناصر)</h2>
      {items.map(item => (
        <div key={item.id}>
          <span>{item.name} × {item.quantity}</span>
          <span>{item.price * item.quantity} ريال</span>
          <button onClick={() => removeItem(item.id)}>إزالة</button>
        </div>
      ))}
      <h3>المجموع: {total.toFixed(2)} ريال</h3>
    </div>
  );
}

تمرين 1: سياق الوضع الداكن

أنشئ نظام وضع داكن كامل مع سياق يحفظ تفضيل المظهر في localStorage.

// المتطلبات:
// 1. إنشاء ThemeContext مع أوضاع فاتح/داكن
// 2. حفظ تفضيل المظهر في localStorage
// 3. تطبيق فئة المظهر على عنصر body
// 4. إنشاء مكون زر التبديل
// 5. توفير hook مخصص useTheme

تمرين 2: تطبيق متعدد اللغات

قم ببناء سياق لغة يدعم الإنجليزية والعربية والفرنسية مع دالة الترجمة.

// المتطلبات:
// 1. إنشاء LanguageContext مع كائن ترجمات
// 2. توفير دالة t() للترجمات
// 3. تغيير اتجاه المستند للغات RTL
// 4. مكون محدد اللغة
// 5. ما لا يقل عن 10 مفاتيح مترجمة

تمرين 3: نظام الإشعارات

أنشئ سياق إشعارات يدير رسائل النجاح/الخطأ/المعلومات مع الإزالة التلقائية.

// المتطلبات:
// 1. NotificationContext مع مصفوفة إشعارات
// 2. الطرق: addNotification, removeNotification, clearAll
// 3. الإزالة التلقائية بعد 3 ثوان
// 4. دعم الأنواع: success, error, info, warning
// 5. مكون NotificationList لعرض الكل

الملخص

  • السياق يوفر طريقة لمشاركة البيانات عبر شجرة المكونات دون تمرير الخصائص
  • إنشاء سياق مع createContext()، توفير مع Provider، استهلاك مع useContext
  • أنماط الموفر المخصصة تغلف منطق السياق وإدارة الحالة
  • Hooks مخصصة مثل useAuth() توفر API أنظف وفحص الأخطاء
  • تقسيم السياقات حسب الاهتمام لتحسين الأداء
  • حالات الاستخدام الشائعة: السمات، المصادقة، اللغة، عربة التسوق
  • تحديثات السياق تتسبب في إعادة عرض جميع المستهلكين - قم بالتحسين عند الحاجة