أساسيات React.js

إدارة الحالة باستخدام Context + useReducer

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

إدارة الحالة باستخدام Context + useReducer

لإدارة الحالة العامة المعقدة، يوفر الجمع بين Context API و useReducer نمطًا قويًا مشابهًا لـ Redux. يمنحك هذا النهج تحديثات حالة يمكن التنبؤ بها وتنظيمًا أفضل للتطبيقات الكبيرة.

فهم نمط Context + Reducer

يدير الـ useReducer hook منطق الحالة المعقد، بينما يجعل Context تلك الحالة متاحة في شجرة المكونات بدون تمرير الـ props. هذا المزيج مثالي للتطبيقات متوسطة الحجم التي تحتاج إلى حالة عامة ولكن لا تتطلب Redux.

متى تستخدم Context + useReducer:
  • إدارة الحالة العامة المعقدة (المصادقة، السمة، السلة)
  • الحالة التي يجب الوصول إليها من قبل العديد من المكونات
  • عندما تحتاج إلى تحديثات حالة يمكن التنبؤ بها
  • التطبيقات متوسطة الحجم التي لا تحتاج لتعقيد Redux

إنشاء مخزن عام

لنبني نظام سلة تسوق باستخدام Context و useReducer:

// contexts/CartContext.jsx import { createContext, useReducer, useContext } from 'react'; // أنواع الإجراءات const CART_ACTIONS = { ADD_ITEM: 'ADD_ITEM', REMOVE_ITEM: 'REMOVE_ITEM', UPDATE_QUANTITY: 'UPDATE_QUANTITY', CLEAR_CART: 'CLEAR_CART', APPLY_DISCOUNT: 'APPLY_DISCOUNT' }; // الحالة الأولية const initialState = { items: [], total: 0, discount: 0, itemCount: 0 }; // دالة الـ reducer function cartReducer(state, action) { switch (action.type) { case CART_ACTIONS.ADD_ITEM: { const existingItem = state.items.find( item => item.id === action.payload.id ); let newItems; if (existingItem) { newItems = state.items.map(item => item.id === action.payload.id ? { ...item, quantity: item.quantity + 1 } : item ); } else { newItems = [...state.items, { ...action.payload, quantity: 1 }]; } return { ...state, items: newItems, total: calculateTotal(newItems, state.discount), itemCount: calculateItemCount(newItems) }; } case CART_ACTIONS.REMOVE_ITEM: { const newItems = state.items.filter( item => item.id !== action.payload ); return { ...state, items: newItems, total: calculateTotal(newItems, state.discount), itemCount: calculateItemCount(newItems) }; } case CART_ACTIONS.UPDATE_QUANTITY: { const newItems = state.items.map(item => item.id === action.payload.id ? { ...item, quantity: action.payload.quantity } : item ).filter(item => item.quantity > 0); return { ...state, items: newItems, total: calculateTotal(newItems, state.discount), itemCount: calculateItemCount(newItems) }; } case CART_ACTIONS.CLEAR_CART: return initialState; case CART_ACTIONS.APPLY_DISCOUNT: return { ...state, discount: action.payload, total: calculateTotal(state.items, action.payload) }; default: return state; } } // دوال مساعدة function calculateTotal(items, discount) { const subtotal = items.reduce( (sum, item) => sum + item.price * item.quantity, 0 ); return subtotal * (1 - discount / 100); } function calculateItemCount(items) { return items.reduce((sum, item) => sum + item.quantity, 0); } // إنشاء الـ context const CartContext = createContext(null); // مكون الـ Provider export function CartProvider({ children }) { const [state, dispatch] = useReducer(cartReducer, initialState); const value = { state, dispatch, actions: CART_ACTIONS }; return ( <CartContext.Provider value={value}> {children} </CartContext.Provider> ); } // Hook مخصص export function useCart() { const context = useContext(CartContext); if (!context) { throw new Error('useCart must be used within CartProvider'); } return context; }

إنشاء منشئي الإجراءات

منشئو الإجراءات يجعلون إرسال الإجراءات أنظف ويوفرون أمان نوع أفضل:

// contexts/CartContext.jsx (تكملة) // منشئي الإجراءات export const cartActions = { addItem: (item) => ({ type: CART_ACTIONS.ADD_ITEM, payload: item }), removeItem: (itemId) => ({ type: CART_ACTIONS.REMOVE_ITEM, payload: itemId }), updateQuantity: (itemId, quantity) => ({ type: CART_ACTIONS.UPDATE_QUANTITY, payload: { id: itemId, quantity } }), clearCart: () => ({ type: CART_ACTIONS.CLEAR_CART }), applyDiscount: (discountPercent) => ({ type: CART_ACTIONS.APPLY_DISCOUNT, payload: discountPercent }) }; // الاستخدام في المكون function ProductCard({ product }) { const { dispatch } = useCart(); const handleAddToCart = () => { dispatch(cartActions.addItem(product)); }; return ( <div className="product-card"> <h3>{product.name}</h3> <p>${product.price}</p> <button onClick={handleAddToCart}> أضف إلى السلة </button> </div> ); }
أفضل ممارسة: استخدم منشئي الإجراءات بدلاً من إنشاء كائنات الإجراءات يدويًا. فهي توفر الاتساق، وتقلل من الأخطاء المطبعية، وتجعل إعادة الهيكلة أسهل. فكر في تصديرها من ملف الـ context.

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

هكذا تستهلك سياق السلة في المكونات:

// components/Cart.jsx import { useCart, cartActions } from '../contexts/CartContext'; function Cart() { const { state, dispatch } = useCart(); const handleUpdateQuantity = (itemId, newQuantity) => { dispatch(cartActions.updateQuantity(itemId, newQuantity)); }; const handleRemoveItem = (itemId) => { dispatch(cartActions.removeItem(itemId)); }; const handleClearCart = () => { if (window.confirm('مسح السلة بالكامل؟')) { dispatch(cartActions.clearCart()); } }; const handleApplyDiscount = () => { dispatch(cartActions.applyDiscount(10)); // خصم 10% }; if (state.items.length === 0) { return <div className="cart-empty">سلتك فارغة</div>; } return ( <div className="cart"> <h2>سلة التسوق ({state.itemCount} عناصر)</h2> <ul className="cart-items"> {state.items.map(item => ( <li key={item.id} className="cart-item"> <div className="item-info"> <h3>{item.name}</h3> <p>${item.price} للواحد</p> </div> <div className="item-quantity"> <button onClick={() => handleUpdateQuantity( item.id, item.quantity - 1 )} > - </button> <span>{item.quantity}</span> <button onClick={() => handleUpdateQuantity( item.id, item.quantity + 1 )} > + </button> </div> <div className="item-total"> ${(item.price * item.quantity).toFixed(2)} </div> <button className="remove-btn" onClick={() => handleRemoveItem(item.id)} > إزالة </button> </li> ))} </ul> <div className="cart-summary"> {state.discount > 0 && ( <p className="discount"> الخصم: {state.discount}% </p> )} <p className="total"> الإجمالي: ${state.total.toFixed(2)} </p> <div className="cart-actions"> <button onClick={handleApplyDiscount}> تطبيق خصم 10% </button> <button onClick={handleClearCart}> مسح السلة </button> <button className="checkout-btn"> الدفع </button> </div> </div> </div> ); }

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

للتطبيقات الأكبر، يمكنك دمج سياقات متعددة:

// contexts/AppProviders.jsx import { CartProvider } from './CartContext'; import { AuthProvider } from './AuthContext'; import { ThemeProvider } from './ThemeContext'; export function AppProviders({ children }) { return ( <ThemeProvider> <AuthProvider> <CartProvider> {children} </CartProvider> </AuthProvider> </ThemeProvider> ); } // App.jsx import { AppProviders } from './contexts/AppProviders'; function App() { return ( <AppProviders> <Router> {/* مسارات التطبيق */} </Router> </AppProviders> ); }
اعتبار الأداء: يعيد Context تصيير جميع المكونات المستهلكة عند تغيير الحالة. للحالة المحدثة بشكل متكرر، فكر في التقسيم إلى سياقات متعددة أو استخدام مكتبات إدارة الحالة مثل Redux. استخدم React.memo() و useMemo() لتحسين التصيير.

نمط متقدم: Context مع محددات

أضف دوال محددة لتحسين تصيير المكونات:

// contexts/CartContext.jsx (متقدم) // المحددات export const cartSelectors = { selectItems: (state) => state.items, selectTotal: (state) => state.total, selectItemCount: (state) => state.itemCount, selectItemById: (state, itemId) => state.items.find(item => item.id === itemId), selectIsInCart: (state, itemId) => state.items.some(item => item.id === itemId) }; // الاستخدام function CartBadge() { const { state } = useCart(); const itemCount = cartSelectors.selectItemCount(state); return ( <div className="cart-badge"> <ShoppingCartIcon /> {itemCount > 0 && ( <span className="badge">{itemCount}</span> )} </div> ); }

تمرين عملي 1: تطبيق مهام بـ Context + useReducer

أنشئ نظام إدارة مهام:

  1. أنشئ TodoContext مع reducer لـ ADD_TODO، TOGGLE_TODO، DELETE_TODO، FILTER_TODOS
  2. ابنِ مكون TodoList يعرض المهام المصفاة
  3. أضف TodoForm لإضافة مهام جديدة
  4. نفذ أزرار الفلترة (الكل، النشط، المكتمل)
  5. أضف وظيفة "مسح المكتمل"

تمرين عملي 2: نظام السمات

ابنِ نظام إدارة سمات شامل:

  1. أنشئ ThemeContext مع سمات فاتح/داكن/مخصص
  2. نفذ إجراءات SWITCH_THEME، SET_CUSTOM_COLORS
  3. أضف مكون ThemeToggle مع قائمة منسدلة
  4. خزن تفضيل السمة في localStorage
  5. طبق متغيرات CSS بناءً على السمة الحالية

تمرين عملي 3: حالة نموذج متعدد الخطوات

أنشئ نموذج معالج مع context:

  1. ابنِ FormContext مع خطوات: معلومات شخصية، العنوان، التفضيلات، المراجعة
  2. نفذ إجراءات NEXT_STEP، PREV_STEP، UPDATE_FIELD، RESET_FORM
  3. أنشئ تنقل مع مؤشر تقدم
  4. تحقق من كل خطوة قبل السماح بالتقدم
  5. اعرض ملخص في خطوة المراجعة

الخلاصة

يوفر Context + useReducer نمطًا قويًا لإدارة الحالة العامة. استخدم أنواع الإجراءات للاتساق، وأنشئ منشئي إجراءات لكود أنظف، وفكر في المحددات لتحسين الأداء. هذا النمط يعمل بشكل رائع للتطبيقات متوسطة الحجم ويوفر فوائد شبيهة بـ Redux دون التعقيد الإضافي.