أساسيات React.js

إدارة الحالة باستخدام Zustand

15 دقيقة الدرس 32 من 40

إدارة الحالة باستخدام Zustand

Zustand هي مكتبة خفيفة الوزن لإدارة الحالة في React توفر واجهة برمجة بسيطة وبديهية. إنها أصغر وأسرع من Redux، مع الحد الأدنى من الكود النمطي ودعم ممتاز لـ TypeScript.

لماذا Zustand؟

يوفر Zustand العديد من المزايا مقارنة بحلول إدارة الحالة التقليدية:

  • الحد الأدنى من الكود النمطي: لا حاجة لموفرات أو إجراءات أو مخفضات
  • حجم صغير: فقط ~1KB مضغوط
  • دعم TypeScript: دعم من الدرجة الأولى لـ TypeScript
  • تجربة المطور: API بسيطة مع تكامل React DevTools
  • الأداء: لا إعادة رسم غير ضرورية

إعداد Zustand

أولاً، قم بتثبيت Zustand في مشروعك:

npm install zustand

أنشئ متجرك الأول:

import { create } from 'zustand'; const useCounterStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), reset: () => set({ count: 0 }), })); // استخدام المتجر في مكون function Counter() { const { count, increment, decrement, reset } = useCounterStore(); return ( <div> <h2>العدد: {count}</h2> <button onClick={increment}>+</button> <button onClick={decrement}>-</button> <button onClick={reset}>إعادة تعيين</button> </div> ); }
ملاحظة: على عكس Context API، لا يتطلب Zustand تغليف تطبيقك بموفر. يمكنك استخدام المتجر مباشرة في أي مكون.

إنشاء متجر للعالم الحقيقي

لنقم بإنشاء متجر سلة تسوق مع ميزات متعددة:

import { create } from 'zustand'; const useCartStore = create((set, get) => ({ items: [], total: 0, // إضافة عنصر إلى السلة addItem: (product) => set((state) => { const existingItem = state.items.find((item) => item.id === product.id); if (existingItem) { // زيادة الكمية إذا كان العنصر موجودًا بالفعل return { items: state.items.map((item) => item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item ), }; } // إضافة عنصر جديد return { items: [...state.items, { ...product, quantity: 1 }], }; }), // إزالة عنصر من السلة removeItem: (productId) => set((state) => ({ items: state.items.filter((item) => item.id !== productId), })), // تحديث كمية العنصر updateQuantity: (productId, quantity) => set((state) => ({ items: state.items.map((item) => item.id === productId ? { ...item, quantity } : item ), })), // مسح السلة clearCart: () => set({ items: [] }), // الحصول على السعر الإجمالي getTotal: () => { const state = get(); return state.items.reduce( (total, item) => total + item.price * item.quantity, 0 ); }, // الحصول على عدد العناصر getItemCount: () => { const state = get(); return state.items.reduce((count, item) => count + item.quantity, 0); }, })); // استخدام متجر السلة function ShoppingCart() { const { items, removeItem, updateQuantity, clearCart, getTotal } = useCartStore(); return ( <div> <h2>سلة التسوق</h2> {items.length === 0 ? ( <p>سلتك فارغة</p> ) : ( <> {items.map((item) => ( <div key={item.id} className="cart-item"> <h3>{item.name}</h3> <p>${item.price}</p> <input type="number" min="1" value={item.quantity} onChange={(e) => updateQuantity(item.id, parseInt(e.target.value)) } /> <button onClick={() => removeItem(item.id)}>إزالة</button> </div> ))} <div className="cart-total"> <h3>الإجمالي: ${getTotal()}</h3> <button onClick={clearCart}>مسح السلة</button> </div> </> )} </div> ); }

استخدام المحددات للأداء

تسمح لك المحددات بالاشتراك في أجزاء محددة من الحالة، مما يمنع إعادة الرسم غير الضرورية:

import { create } from 'zustand'; const useUserStore = create((set) => ({ user: { name: 'أحمد محمد', email: 'ahmed@example.com', settings: { theme: 'light', notifications: true, }, }, updateUser: (updates) => set((state) => ({ user: { ...state.user, ...updates }, })), updateSettings: (settings) => set((state) => ({ user: { ...state.user, settings: { ...state.user.settings, ...settings }, }, })), })); // المكون يعيد الرسم فقط عند تغيير السمة function ThemeToggle() { const theme = useUserStore((state) => state.user.settings.theme); const updateSettings = useUserStore((state) => state.updateSettings); return ( <button onClick={() => updateSettings({ theme: theme === 'light' ? 'dark' : 'light', }) } > تبديل السمة ({theme}) </button> ); } // المكون يعيد الرسم فقط عند تغيير الاسم function UserGreeting() { const name = useUserStore((state) => state.user.name); return <h1>مرحباً، {name}!</h1>; }
نصيحة: استخدم المحددات لتحسين الأداء. ستعيد المكونات الرسم فقط عند تغيير الحالة المحددة، وليس عند تحديث أي جزء من المتجر.

البرمجيات الوسيطة: Persist

تسمح لك البرمجية الوسيطة persist بحفظ متجرك في localStorage أو sessionStorage:

import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; const usePreferencesStore = create( persist( (set) => ({ theme: 'light', language: 'ar', fontSize: 16, setTheme: (theme) => set({ theme }), setLanguage: (language) => set({ language }), setFontSize: (fontSize) => set({ fontSize }), resetPreferences: () => set({ theme: 'light', language: 'ar', fontSize: 16, }), }), { name: 'user-preferences', // مفتاح localStorage storage: createJSONStorage(() => localStorage), // الافتراضي هو localStorage } ) ); // استخدام متجر مستمر function Preferences() { const { theme, language, fontSize, setTheme, setLanguage, setFontSize } = usePreferencesStore(); return ( <div> <h2>تفضيلات المستخدم</h2> <label> السمة: <select value={theme} onChange={(e) => setTheme(e.target.value)}> <option value="light">فاتح</option> <option value="dark">داكن</option> </select> </label> <label> اللغة: <select value={language} onChange={(e) => setLanguage(e.target.value)}> <option value="ar">العربية</option> <option value="en">الإنجليزية</option> <option value="fr">الفرنسية</option> </select> </label> <label> حجم الخط: <input type="range" min="12" max="24" value={fontSize} onChange={(e) => setFontSize(parseInt(e.target.value))} /> {fontSize}px </label> </div> ); }
تحذير: كن حذرًا بشأن ما تحفظه. يجب عدم تخزين البيانات الحساسة مثل كلمات المرور أو رموز API في localStorage.

البرمجيات الوسيطة: DevTools

اربط متجر Zustand الخاص بك بـ Redux DevTools لتصحيح الأخطاء:

import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; const useTodoStore = create( devtools( (set) => ({ todos: [], addTodo: (text) => set( (state) => ({ todos: [ ...state.todos, { id: Date.now(), text, completed: false }, ], }), false, 'todos/add' // اسم الإجراء في DevTools ), toggleTodo: (id) => set( (state) => ({ todos: state.todos.map((todo) => todo.id === id ? { ...todo, completed: !todo.completed } : todo ), }), false, 'todos/toggle' ), removeTodo: (id) => set( (state) => ({ todos: state.todos.filter((todo) => todo.id !== id), }), false, 'todos/remove' ), }), { name: 'TodoStore' } ) );

دمج البرمجيات الوسيطة المتعددة

يمكنك دمج persist و devtools middleware:

import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import { devtools } from 'zustand/middleware'; const useAuthStore = create( devtools( persist( (set) => ({ user: null, token: null, isAuthenticated: false, login: (user, token) => set( { user, token, isAuthenticated: true, }, false, 'auth/login' ), logout: () => set( { user: null, token: null, isAuthenticated: false, }, false, 'auth/logout' ), updateUser: (updates) => set( (state) => ({ user: { ...state.user, ...updates }, }), false, 'auth/updateUser' ), }), { name: 'auth-storage', storage: createJSONStorage(() => localStorage), } ), { name: 'AuthStore' } ) ); // مكون المسار المحمي function ProtectedRoute({ children }) { const isAuthenticated = useAuthStore((state) => state.isAuthenticated); if (!isAuthenticated) { return <Navigate to="/login" />; } return children; }
نصيحة: عند استخدام برمجيات وسيطة متعددة، الترتيب مهم. بشكل عام، استخدم: devtools(persist(store)) لضمان التقاط DevTools لجميع الإجراءات بما في ذلك المستمرة.

الإجراءات غير المتزامنة وتكامل API

التعامل مع العمليات غير المتزامنة في متاجر Zustand:

import { create } from 'zustand'; const useProductStore = create((set) => ({ products: [], loading: false, error: null, fetchProducts: async () => { set({ loading: true, error: null }); try { const response = await fetch('https://api.example.com/products'); if (!response.ok) { throw new Error('Failed to fetch products'); } const data = await response.json(); set({ products: data, loading: false }); } catch (error) { set({ error: error.message, loading: false }); } }, createProduct: async (product) => { set({ loading: true, error: null }); try { const response = await fetch('https://api.example.com/products', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(product), }); if (!response.ok) { throw new Error('Failed to create product'); } const data = await response.json(); set((state) => ({ products: [...state.products, data], loading: false, })); } catch (error) { set({ error: error.message, loading: false }); } }, deleteProduct: async (id) => { set({ loading: true, error: null }); try { const response = await fetch(`https://api.example.com/products/${id}`, { method: 'DELETE', }); if (!response.ok) { throw new Error('Failed to delete product'); } set((state) => ({ products: state.products.filter((p) => p.id !== id), loading: false, })); } catch (error) { set({ error: error.message, loading: false }); } }, }));
تمرين 1: أنشئ متجر إشعارات باستخدام Zustand يمكنه إضافة وإزالة وإخفاء الإشعارات تلقائيًا بعد 3 ثوانٍ. قم بتضمين أنواع الإشعارات (نجاح، خطأ، تحذير، معلومات) واحفظ معرفات الإشعارات المرفوضة في localStorage.
تمرين 2: قم ببناء متجر نموذج متعدد الخطوات يتتبع الخطوة الحالية وبيانات النموذج لكل خطوة وأخطاء التحقق. نفذ وظائف للانتقال إلى الخطوات التالية/السابقة وحفظ بيانات النموذج وإرسال النموذج الكامل.
تمرين 3: أنشئ متجر بحث وتصفية لكتالوج منتجات. قم بتضمين مرشحات للفئة ونطاق الأسعار والتقييم. نفذ بحثًا مع تأخير واحفظ استعلام البحث والمرشحات الأخيرة باستخدام persist middleware.