React: إدارة الحالة: Zustand / Redux
آخر تحديث: 2026-08-26
تم تنفيذ ميزة عربة التسوق الخاصة بـ «توم» في البداية عن طريق تمرير البيانات طبقةً تلو الأخرى باستخدام
useStateوprops، وهو ما كان بالكاد يعمل. ومع ذلك، مع تزايد عمق شجرة المكونات (App → Header → CartIcon، App → ProductList → ProductCard → AddToCartButton)، أصبح تعديل حالة عربة التسوق يتطلب تمرير دوال الاستدعاء عبر خمس طبقات من المكونات — وكان أدنى خطأ قد يؤدي إلى فقدان دالة الاستدعاء أو تمريرها بشكل غير صحيح. فأدرك توم أنه: هناك حاجة إلى حل شامل لإدارة الحالة، يتيح لأي مكون قراءة الحالة المشتركة وكتابتها مباشرةً.
1. ما ستتعلمه
- Zustand: واجهة برمجة التطبيقات الأساسية لإدارة الحالة بطريقة بسيطة
- مسار عمل Redux Toolkit: Slice → Store → Provider → Hooks
- معايير الاختيار والسيناريوهات التي تنطبق على الحلين
- استخدام برمجيات الوسيطة الخاصة بالتخزين الدائم
- إدارة الحالة الآمنة من حيث الأنواع في TypeScript
2. الرسوم التخطيطية المفاهيمية
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.
npm install zustand
▶ المثال 1: متجر «Zustand Shopping Cart»
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:
- Zero Provider: لا حاجة إلى تضمين
<Provider>؛ ما عليك سوى استدعاء الـ"هوك" مباشرةً داخل أي مكون - الاشتراك الانتقائي في العناصر الفرعية: لا يُطلق
useCartStore(state => state.items.length)عملية إعادة العرض إلا عند تغيرitems.length، مما يوفر أداءً أفضل مقارنةً بـ Context - set + get:
setيقوم بتحديث الحالة؛getيقرأ الحالة الحالية (مفيد لقراءة قيم الحالة الأخرى داخل الإجراء)
▶ المثال 2: برمجيات الوسيطة الخاصة باستمرار الحالة
يريد توم أن يتمكن المستخدمون من تحديث الصفحة دون فقدان بيانات سلة التسوق الخاصة بهم. يمكن لبرنامج الوسيط persist في Zustand مزامنة المتجر تلقائيًا مع localStorage.
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 في المتصفح.
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 سير عمل موحدًا لإدارة الحالة يفرض نمطًا متسقًا على مستوى الفريق بأكمله.
npm install @reduxjs/toolkit react-redux
▶ المثال 4: سير العمل الكامل لمجموعة أدوات Redux
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 دعمًا مدمجًا لمراحل دورة حياة العمليات غير المتزامنة (قيد الانتظار، تم التنفيذ، ورفض).
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. كود أقل، عبء ذهني أقل، وما يكفي فقط.
- المشاريع الكبيرة / الفرق المتعددة / المعايير الصارمة المطلوبة → Redux Toolkit. تضمن العمليات الموحدة اتساق أنماط البرمجة عبر الفرق المختلفة.
- استخدام مزيج من الاثنين هو أيضًا نهج شائع: استخدم Zustand للحالة العامة المشتركة بين الصفحات، و Redux Toolkit للوحدات الوظيفية المعقدة.
❓ أسئلة شائعة
createSlice فيقوم تلقائيًا بإنشاء أنواع الإجراءات ومنشئي الإجراءات، configureStore ويقوم تلقائيًا بدمج البرامج الوسيطة و DevTools، و createAsyncThunk ويبسط سير العمل غير المتزامن. وهذا يقلل من حجم الكود بنسبة 60٪ تقريبًا.Zustand خارج المكونات؟useCartStore.getState() استرجاع الحالة الحالية من أي مكان، useCartStore.setState(...) تحديث الحالة من أي مكان. وهذا مفيد جدًّا للوصول إلى الحالة في حراس المسارات (route guards) والمعترضات (interceptors) وعمليات الاستدعاء المرتدة لـ WebSocket.useReducer احتياجاتك؟useReducer مناسبة لمنطق الحالة المعقد داخل مكون واحد أو في نطاق محدد. لا ينبغي عليك استخدام Zustand أو Redux Toolkit إلا عندما يتعين مشاركة الحالة عبر عدة مكونات غير مرتبطة ببعضها، أو عندما يلزم الاحتفاظ بها بشكل دائم، أو عندما تحتاج إلى برمجيات وسيطة. لا تستخدمها لمجرد استخدامها.set في Zustand ودالة setState في React؟set في Zustand بإجراء دمج سطحي بشكل افتراضي (على غرار دالة setState في مكونات الفئات)، بينما تقوم مُخفِّضات Redux Toolkit، المستندة إلى Immer، بإجراء تحديثات عميقة وثابتة. يمكن أيضًا استخدام Zustand مع البرمجيات الوسيطة immer لتحقيق صيغة تحديث قابلة للتغيير مشابهة لتلك الموجودة في Redux Toolkit. لكلتا الطريقتين مزاياها وعيوبها من حيث الاستخدام؛ اختر ما يناسب تفضيلات فريقك.📖 ملخص
- تشتهر «Zustand» بواجهة برمجة التطبيقات (API) البسيطة:
create(set => ({ state, actions }))، وعدم وجود مزودي الخدمة، والاشتراكات الفرعية الانتقائية - برنامج الوسيط «Persistence»
persist: مزامنة الحالة تلقائيًا مع localStorage بسطر واحد من التعليمات البرمجية - تعمل البرمجيات الوسيطة «devtools» على دمج «Zustand» مع «Redux DevTools»، مما يتيح إمكانية تصحيح الأخطاء باستخدام ميزة «السفر عبر الزمن»
- توفر Redux Toolkit سير عمل موحدًا: createSlice → configureStore → Provider → useSelector/useDispatch
createAsyncThunkيدير تلقائيًا الحالات الثلاث للعمليات غير المتزامنة (قيد الانتظار، تم تنفيذها، ورفضت)- بالنسبة للمشاريع الصغيرة، نوصي باستخدام Zustand؛ أما بالنسبة للمشاريع الكبيرة، فنوصي باستخدام Redux Toolkit. يمكنك استخدامهما معًا.
- توفر أنظمة البرمجيات الوسيطة (الدوال المتداخلة في Zustand مقابل نمط «المنشئ» في Redux Toolkit) قابلية توسعة مرنة
- يُعد «Zustand» مناسبًا لـ 80% من سيناريوهات مشاركة الحالة، في حين أن «Redux Toolkit» مناسب للوحدات الوظيفية المعقدة التي تتطلب مواصفات صارمة.
- بغض النظر عن الخيار الذي تختاره، نوصي بوضع اللمسات الأخيرة على استراتيجية إدارة الحالات في مرحلة مبكرة من المشروع لتجنب تكاليف الترحيل المرتبطة بالتغيير في منتصف الطريق.
📝 تمارين
- أنشئ متجر «Todo Store» باستخدام Zustand: يجب أن يدعم إضافة المهام، وتغيير حالة الإنجاز، وحذف المهام، ومسح المهام المنجزة، وحساب عدد المهام غير المنجزة. استخدمه في كل من مكون القائمة ومكون الإحصائيات للتحقق من فعالية الاشتراك الانتقائي.
- أضف البرمجية الوسيطة
persistإلى Todo Store للتحقق مما إذا كانت البيانات تُحتفظ بها بعد تحديث الصفحة. - تنفيذ وحدة إدارة المستخدمين باستخدام Redux Toolkit: استخدم
createAsyncThunkلمحاكاة استرداد قائمة بالمستخدمين (مع تأخير مدته ثانية واحدة قبل إرجاع البيانات)، وإدارة الحالات الثلاث (التحميل، والخطأ، والبيانات)، وعرض حالة التحميل، وحالة الخطأ، وقائمة البيانات في المكون.