React: إدارة الحالة: Zustand / Redux

آخر تحديث: 2026-08-26

تم تنفيذ ميزة عربة التسوق الخاصة بـ «توم» في البداية عن طريق تمرير البيانات طبقةً تلو الأخرى باستخدام useState وprops، وهو ما كان بالكاد يعمل. ومع ذلك، مع تزايد عمق شجرة المكونات (App → Header → CartIcon، App → ProductList → ProductCard → AddToCartButton)، أصبح تعديل حالة عربة التسوق يتطلب تمرير دوال الاستدعاء عبر خمس طبقات من المكونات — وكان أدنى خطأ قد يؤدي إلى فقدان دالة الاستدعاء أو تمريرها بشكل غير صحيح. فأدرك توم أنه: هناك حاجة إلى حل شامل لإدارة الحالة، يتيح لأي مكون قراءة الحالة المشتركة وكتابتها مباشرةً.


1. ما ستتعلمه



2. الرسوم التخطيطية المفاهيمية

100%
flowchart LR
    subgraph Zustand[Zustand Pattern]
        A1[create API] --> B1[Store<br/>state + actions]
        B1 --> C1[Component A<br/>Subscribe Now]
        B1 --> C2[Component B<br/>Subscribe Now]
        B1 --> C3[Component C<br/>Subscribe Now]
    end

    subgraph Redux[Redux Toolkit Pattern]
        A2[createSlice] --> B2[Slice<br/>reducers + actions]
        B2 --> C4[configureStore]
        C4 --> D[Provider<br/>Wrapper Root Component]
        D --> E1[useSelector<br/>Read Status]
        D --> E2[useDispatch<br/>Trigger an update]
    end

    style A1 fill:#e1f5fe,stroke:#0288d1
    style B1 fill:#fff3e0,stroke:#f57c00
    style C4 fill:#e8f5e9,stroke:#388e3c
    style D fill:#c8e6c9,stroke:#2e7d32

على اليسار: Zustand — لا يوجد مزود خدمة، واشتراكات مباشرة، وواجهة برمجة تطبيقات (API) بسيطة. على اليمين: Redux Toolkit — يوفر سير عمل متكاملًا ومناسبًا للتعاون بين الفرق الكبيرة.



3. سيناريو واقعي

تتطلب عربة التسوق الخاصة بـ«توم» عدة ميزات: زر «إضافة إلى عربة التسوق» في صفحة قائمة المنتجات، وأيقونة عربة التسوق في العنوان (تعرض عدد العناصر)، وصفحة عربة التسوق (تعرض قائمة المنتجات والسعر الإجمالي)، والقدرة على مسح محتويات عربة التسوق بعد إتمام الطلب. تنتشر هذه المكونات عبر مستويات مختلفة من شجرة المكونات، ولا يعد تمريرها باستخدام useState + props أمرًا مرهقًا فحسب، بل إنه عرضة للأخطاء أيضًا.

(1) لماذا لا يمكن استخدام «السياق» كبديل؟

إن ميزة «Context» في React تمنع بالفعل تمرير الخصائص (props) عبر مستويات متعددة، لكنها تعاني من مشكلتين خطيرتين:

تعالج مكتبات إدارة الحالة المخصصة (Zustand، Redux Toolkit) مشكلات الأداء من خلال آلية الاشتراك الانتقائي — حيث تستمع المكونات فقط إلى أجزاء الحالة التي تهمها، ولا تؤدي التحديثات غير ذات الصلة إلى إعادة العرض.

الحل الأداء المزود الاشتراك الانتقائي منحنى التعلم حجم الحزمة
useState + props ✅ سريع محليًّا لا شيء غير متوفر منخفض جدًّا 0
السياق + useReducer ❌ إعادة عرض كاملة مطلوب منخفض 0
الحالة ✅ اشتراك اختياري غير مطلوب ✅ تطبيق أصلي منخفض ~1 كيلوبايت
Redux Toolkit ✅ useSelector مطلوب في ~12 كيلوبايت

(2) الحالة: النهج البسيط

الفكرة الأساسية وراء Zustand هي «هوك واحد يقوم بكل شيء»: create() فهو يحدد الحالة والإجراءات في آن واحد، ويمكن استخدام الهوك الذي يتم إرجاعه مباشرةً في أي مكون دون الحاجة إلى تغليفه في Provider.

BASH
npm install zustand

▶ المثال 1: متجر «Zustand Shopping Cart»

JSX
import { create } from 'zustand'

// Definition store Types of(TypeScript Project Recommendations)
/*
type CartItem = { id: number; name: string; price: number; qty: number }
type CartStore = {
  items: CartItem[]
  total: number
  addItem: (product: Omit<CartItem, 'qty'>) => void
  removeItem: (id: number) => void
  updateQty: (id: number, qty: number) => void
  clearCart: () => void
}
*/

// Create store — state and  actions They're all defined here
const useCartStore = create((set, get) => ({
  // --- state ---
  items: [],
  total: 0,

  // --- actions ---
  addItem: (product) => set((state) => {
    const existing = state.items.find(i => i.id === product.id)
    const newItems = existing
      ? state.items.map(i =>
          i.id === product.id ? { ...i, qty: i.qty + 1 } : i
        )
      : [...state.items, { ...product, qty: 1 }]
    return {
      items: newItems,
      total: newItems.reduce((sum, i) => sum + i.price * i.qty, 0)
    }
  }),

  removeItem: (id) => set((state) => {
    const newItems = state.items.filter(i => i.id !== id)
    return {
      items: newItems,
      total: newItems.reduce((sum, i) => sum + i.price * i.qty, 0)
    }
  }),

  updateQty: (id, qty) => set((state) => {
    const newItems = qty <= 0
      ? state.items.filter(i => i.id !== id)
      : state.items.map(i => i.id === id ? { ...i, qty } : i)
    return {
      items: newItems,
      total: newItems.reduce((sum, i) => sum + i.price * i.qty, 0)
    }
  }),

  clearCart: () => set({ items: [], total: 0 })
}))

// Use in any component
function CartIcon() {
  // Subscribe to items.length only,The page will only be re-rendered when the shopping cart list changes.
  const itemCount = useCartStore(state => state.items.length)

  return <span className="cart-badge">Shopping Cart ({itemCount})</span>
}

function ProductCard({ product }) {
  const addItem = useCartStore(state => state.addItem)

  return (
    <div>
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button onClick={() => addItem(product)}>Add to Cart</button>
    </div>
  )
}

function CartPage() {
  const { items, total, updateQty, removeItem, clearCart } = useCartStore()

  if (items.length === 0) return <p>Your shopping cart is empty</p>

  return (
    <div>
      <h2>Shopping Cart</h2>
      {items.map(item => (
        <div key={item.id}>
          <span>{item.name}</span>
          <input
            type="number"
            value={item.qty}
            min="0"
            onChange={e => updateQty(item.id, Number(e.target.value))}
          />
          <span>${item.price * item.qty}</span>
          <button onClick={() => removeItem(item.id)}>Delete</button>
        </div>
      ))}
      <hr />
      <p><strong>Total:${total}</strong></p>
      <button onClick={clearCart}>Empty Cart</button>
    </div>
  )
}

الميزات الرئيسية لـ Zustand:

▶ المثال 2: برمجيات الوسيطة الخاصة باستمرار الحالة

يريد توم أن يتمكن المستخدمون من تحديث الصفحة دون فقدان بيانات سلة التسوق الخاصة بهم. يمكن لبرنامج الوسيط persist في Zustand مزامنة المتجر تلقائيًا مع localStorage.

JSX
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

// persist Middleware Package store Definition
const useCartStore = create(
  persist(
    (set, get) => ({
      items: [],
      total: 0,
      addItem: (product) => set((state) => {
        const existing = state.items.find(i => i.id === product.id)
        const newItems = existing
          ? state.items.map(i =>
              i.id === product.id ? { ...i, qty: i.qty + 1 } : i
            )
          : [...state.items, { ...product, qty: 1 }]
        return {
          items: newItems,
          total: newItems.reduce((sum, i) => sum + i.price * i.qty, 0)
        }
      }),
      removeItem: (id) => set((state) => {
        const newItems = state.items.filter(i => i.id !== id)
        return {
          items: newItems,
          total: newItems.reduce((sum, i) => sum + i.price * i.qty, 0)
        }
      }),
      clearCart: () => set({ items: [], total: 0 })
    }),
    {
      name: 'cart-storage',  // localStorage 's  key
      // Store all fields by default,That's fine, too. partialize Select which fields to store
      // partialize: (state) => ({ items: state.items }),
    }
  )
)

// The way it works remains exactly the same
function CartIcon() {
  const itemCount = useCartStore(state => state.items.length)
  return <span>Shopping Cart ({itemCount})</span>
}

// localStorage Data Format Stored in the System:
// key: 'cart-storage'
// value: '{"state":{"items":[...],"total":99},"version":0}'
▶ جرّب الكود

كيف تعمل برمجيات الوسيطة «Persistence»: تتم مزامنة أي تغييرات في الحالة تلقائيًا مع localStorage، ويتم استعادة البيانات تلقائيًا من localStorage عند بدء تشغيل التطبيق. وعندما يقوم المستخدم بتحديث الصفحة، يتم الحفاظ على حالة سلة التسوق بالكامل.

▶ المثال 3: تصحيح الأخطاء في Zustand DevTools

خلال مرحلة التطوير، أراد توم الاطلاع على سجل تغييرات الحالات، على غرار Redux DevTools. تتكامل البرمجيات الوسيطة devtools الخاصة بـ Zustand بسلاسة مع ملحقات DevTools في المتصفح.

JSX
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'

// Using Multiple Middleware Components Simultaneously:Combine using a nested approach
const useStore = create(
  devtools(
    persist(
      (set) => ({
        count: 0,
        increment: () => set(state => ({ count: state.count + 1 })),
        decrement: () => set(state => ({ count: state.count - 1 })),
      }),
      { name: 'app-storage' }
    ),
    { name: 'AppStore' }  // DevTools As shown in store Name
  )
)

// Open in a browser Redux DevTools Expand
// As you can see "AppStore" All items under this tag state Change Log
// Supports time-travel debugging(Jump to any historical state)
▶ جرّب الكود

ميزات نظام البرمجيات الوسيطة Zustand: يتم تنفيذ البرمجيات الوسيطة بشكل تسلسلي من الطبقة الخارجية إلى الطبقة الداخلية من خلال مجموعات من الوظائف المتداخلة. يشير devtools(persist(...)) إلى أن DevTools يتم توصيله أولاً، يليه معالجة الاستمرارية. يمكن دمج مكونات البرمجيات الوسيطة المتعددة بمرونة لتلبية الاحتياجات المختلفة. على سبيل المثال، يمكنك إضافة البرمجيات الوسيطة immer لتمكين بناء الجمل المتغير، أو إنشاء برمجيات وسيطة مخصصة لتسجيل كل إجراء.



4. Redux Toolkit: حل مخصص للمؤسسات

عندما ينمو المشروع إلى درجة تتطلب تعاون عدة فرق، قد تصبح مرونة Zustand مشكلة في الواقع — فقد يكتب المطورون الإجراءات بأساليب غير متسقة. يوفر Redux Toolkit سير عمل موحدًا لإدارة الحالة يفرض نمطًا متسقًا على مستوى الفريق بأكمله.

BASH
npm install @reduxjs/toolkit react-redux

▶ المثال 4: سير العمل الكامل لمجموعة أدوات Redux

JSX
import { createSlice, configureStore } from '@reduxjs/toolkit'
import { Provider, useSelector, useDispatch } from 'react-redux'

// ========== 1. Create Slice(Definition state + reducers)==========
const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0, history: [] },
  reducers: {
    increment: (state) => {
      state.value += 1
      state.history.push(`+1 → ${state.value}`)
    },
    decrement: (state) => {
      state.value -= 1
      state.history.push(`-1 → ${state.value}`)
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
      state.history.push(`+${action.payload} → ${state.value}`)
    },
    reset: (state) => {
      state.value = 0
      state.history = []
    }
  }
})

// Automatically generated action creators
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions

// ========== 2. Layout Store ==========
const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
    // More can be added later. slice:
    // cart: cartSlice.reducer,
    // user: userSlice.reducer,
  }
})

// ========== 3. Provider Inject ==========
function App() {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  )
}

// ========== 4. Used in components ==========
function Counter() {
  // Selective Reading of Characters
  const value = useSelector(state => state.counter.value)
  const history = useSelector(state => state.counter.history)
  const dispatch = useDispatch()

  return (
    <div>
      <h2>Counter:{value}</h2>
      <div>
        <button onClick={() => dispatch(increment())}>+1</button>
        <button onClick={() => dispatch(decrement())}>-1</button>
        <button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
        <button onClick={() => dispatch(reset())}>Reset</button>
      </div>
      <div>
        <h3>Transaction History</h3>
        <ul>
          {history.slice(-5).map((h, i) => (
            <li key={i}>{h}</li>
          ))}
        </ul>
      </div>
    </div>
  )
}

المفاهيم الأساسية لـ Redux Toolkit:

المفهوم الوظيفة التشبيه
createSlice إنشاء مجموعة من الحالات والمُختزِّلات ذات الصلة درج الملفات: التنظيم والتخزين
configureStore دمج جميع الشرائح لإنشاء مخزن شامل خزانة الملفات: الإدارة المركزية
Provider إدراج المتجر في شجرة مكونات React سلك الطاقة: قم بتوصيله
useSelector قراءة جزء من الحالة من المخزن فتح الدرج لاسترداد ملف
useDispatch تشغيل إجراء لتحديث الحالة إصدار أمر للمسؤول لاتخاذ إجراء

▶ المثال 5: العمليات غير المتزامنة في Redux Toolkit

غالبًا ما تنطوي تحديثات الحالة في المشاريع الواقعية على منطق غير متزامن (مثل تسجيل الدخول أو استرداد معلومات المستخدم). يتضمن Redux Toolkit دعمًا مدمجًا لمراحل دورة حياة العمليات غير المتزامنة (قيد الانتظار، تم التنفيذ، ورفض).

JSX
import { createSlice, configureStore, createAsyncThunk } from '@reduxjs/toolkit'
import { Provider, useSelector, useDispatch } from 'react-redux'

// 1. Defining Asynchronous thunk(Automatically Generated pending/fulfilled/rejected Three types action)
const fetchUser = createAsyncThunk(
  'user/fetchUser',      // action Type prefix
  async (userId, { rejectWithValue }) => {
    try {
      const response = await fetch(`/api/users/${userId}`)
      if (!response.ok) throw new Error('Failed to retrieve the user')
      return await response.json()
    } catch (err) {
      return rejectWithValue(err.message)
    }
  }
)

// 2. Create slice
const userSlice = createSlice({
  name: 'user',
  initialState: {
    data: null,
    loading: false,
    error: null
  },
  reducers: {
    clearUser: (state) => {
      state.data = null
      state.error = null
    }
  },
  // Handling Asynchronous Operations thunk The Three States of
  extraReducers: (builder) => {
    builder
      .addCase(fetchUser.pending, (state) => {
        state.loading = true
        state.error = null
      })
      .addCase(fetchUser.fulfilled, (state, action) => {
        state.loading = false
        state.data = action.payload
      })
      .addCase(fetchUser.rejected, (state, action) => {
        state.loading = false
        state.error = action.payload
      })
  }
})

export const { clearUser } = userSlice.actions

// 3. Layout store
const store = configureStore({
  reducer: { user: userSlice.reducer }
})

// 4. Used in components
function UserProfile({ userId }) {
  const dispatch = useDispatch()
  const { data, loading, error } = useSelector(state => state.user)

  return (
    <div>
      <button onClick={() => dispatch(fetchUser(userId))}>Load Users</button>
      {loading && <p>Loading......</p>}
      {error && <p>Error:{error}</p>}
      {data && <p>{data.name} — {data.email}</p>}
    </div>
  )
}

تتولى createAsyncThunk معالجة الحالات الثلاث تلقائيًا: عند بدء الطلب، تقوم بإرسال pending → في حالة النجاح، تقوم بإرسال fulfilled (حاملةً البيانات) → في حالة الفشل، تقوم بإرسال rejected (حاملةً رسالة الخطأ). يحتاج المطورون فقط إلى تحديد كيفية تغير الحالة لكل شرط داخل extraReducers. مقارنةً باستخدام useEffect وfetch مباشرةً، تشمل مزايا هذا النمط: التتبع التلقائي لحالة الطلب، والإجراءات التي يمكن تسجيلها بواسطة DevTools، والقدرة على اشتراك مكونات متعددة في نتائج نفس الثانك.



5. مقارنة النماذج

البعد الحالة Redux Toolkit
حجم الكود ضئيل جدًّا — مجرد دالة واحدة create() معتدل — المجموعة الكاملة من وظائف slice و store و Provider
منحنى التعلم منخفض (10 دقائق للبدء) متوسط (يتطلب فهم المُخفِّضات، والإجراءات، والتوزيع، و«الثانكس»)
المزود غير مطلوب يجب أن تكون حزمة «المزود»
الاشتراك الانتقائي الدعم الأصلي، وظائف المحدد useSelector + المقارنة السطحية
TypeScript متوافق مع اللغات الأصلية جيد، مع استنتاج الأنواع
البرمجيات الوسيطة persist / devtools / immer، إلخ. ثانك مدمج + ساغا/إبيك قابلة للتوسيع
المعالجة غير المتزامنة يتم تنفيذها يدويًّا في الإجراء مدمجة مع createAsyncThunk
حجم الحزمة ~1 كيلوبايت ~12 كيلوبايت
حالات الاستخدام التطبيقات الصغيرة والمتوسطة الحجم، ومشاركة الحالة بين المكونات التطبيقات الكبيرة، وتدفقات البيانات المعقدة، والتعاون بين الفرق
DevTools مدعوم (يتطلب برامج وسيطة) دعم أصلي، تصحيح الأخطاء باستخدام ميزة «السفر عبر الزمن»

نصائح للاختيار:



❓ أسئلة شائعة

س كيف أختار بين Zustand وContext + useReducer؟
ج يقدم Zustand أداءً أفضل — فعندما يتم تحديث Context، يتم إعادة عرض جميع المستهلكين، في حين يدعم Zustand الاشتراكات الانتقائية الدقيقة. كما يتطلب Zustand كودًا أقل ويتجنب استخدام موفري الخدمة المتداخلة. يُعد Context + useReducer مناسبًا للسيناريوهات البسيطة للغاية (مثل تبديل السمات)، ولكن في معظم الحالات، يُعد Zustand الخيار الأفضل.
س ما الذي يبسطه Redux Toolkit مقارنةً بـ Redux التقليدي؟
ج يتطلب Redux التقليدي كتابة ثوابت أنواع الإجراءات (action type constants) ومنشئي الإجراءات (action creators) وعبارات switch-case الخاصة بالمُخفِّض (reducer) يدويًّا، بالإضافة إلى تكوين Redux DevTools والبرمجيات الوسيطة (middleware) بشكل منفصل. أما Redux Toolkit، createSlice فيقوم تلقائيًا بإنشاء أنواع الإجراءات ومنشئي الإجراءات، configureStore ويقوم تلقائيًا بدمج البرامج الوسيطة و DevTools، و createAsyncThunk ويبسط سير العمل غير المتزامن. وهذا يقلل من حجم الكود بنسبة 60٪ تقريبًا.
س هل يمكن استخدام مخزن Zustand خارج المكونات؟
ج نعم. useCartStore.getState() استرجاع الحالة الحالية من أي مكان، useCartStore.setState(...) تحديث الحالة من أي مكان. وهذا مفيد جدًّا للوصول إلى الحالة في حراس المسارات (route guards) والمعترضات (interceptors) وعمليات الاستدعاء المرتدة لـ WebSocket.
س هل يمكن استخدام المكتبتين معًا في نفس المشروع في الوقت نفسه؟
ج بالتأكيد. تبدأ العديد من المشاريع باستخدام Zustand، ثم يتم إدخال Redux Toolkit لاحقًا عندما تزداد تعقيدات وحدة معينة. ولا يوجد تعارض بينهما — حيث تتولى Zustand إدارة الحالة العامة البسيطة (السمات، ومعلومات المستخدم)، بينما تتولى Redux Toolkit إدارة وحدات الأعمال المعقدة (عمليات الطلبات، وسير عمل الموافقة).
س هل يجب عليك استخدام مكتبة لإدارة الحالة فقط عندما لا تلبي useReducer احتياجاتك؟
ج useReducer مناسبة لمنطق الحالة المعقد داخل مكون واحد أو في نطاق محدد. لا ينبغي عليك استخدام Zustand أو Redux Toolkit إلا عندما يتعين مشاركة الحالة عبر عدة مكونات غير مرتبطة ببعضها، أو عندما يلزم الاحتفاظ بها بشكل دائم، أو عندما تحتاج إلى برمجيات وسيطة. لا تستخدمها لمجرد استخدامها.
س ما الفرق بين دالة set في Zustand ودالة setState في React؟
ج تقوم دالة set في Zustand بإجراء دمج سطحي بشكل افتراضي (على غرار دالة setState في مكونات الفئات)، بينما تقوم مُخفِّضات Redux Toolkit، المستندة إلى Immer، بإجراء تحديثات عميقة وثابتة. يمكن أيضًا استخدام Zustand مع البرمجيات الوسيطة immer لتحقيق صيغة تحديث قابلة للتغيير مشابهة لتلك الموجودة في Redux Toolkit. لكلتا الطريقتين مزاياها وعيوبها من حيث الاستخدام؛ اختر ما يناسب تفضيلات فريقك.

📖 ملخص


📝 تمارين

  1. أنشئ متجر «Todo Store» باستخدام Zustand: يجب أن يدعم إضافة المهام، وتغيير حالة الإنجاز، وحذف المهام، ومسح المهام المنجزة، وحساب عدد المهام غير المنجزة. استخدمه في كل من مكون القائمة ومكون الإحصائيات للتحقق من فعالية الاشتراك الانتقائي.
  2. أضف البرمجية الوسيطة persist إلى Todo Store للتحقق مما إذا كانت البيانات تُحتفظ بها بعد تحديث الصفحة.
  3. تنفيذ وحدة إدارة المستخدمين باستخدام Redux Toolkit: استخدم createAsyncThunk لمحاكاة استرداد قائمة بالمستخدمين (مع تأخير مدته ثانية واحدة قبل إرجاع البيانات)، وإدارة الحالات الثلاث (التحميل، والخطأ، والبيانات)، وعرض حالة التحميل، وحالة الخطأ، وقائمة البيانات في المكون.
Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%