من useState إلى Redux وZustand، متى تستخدم كل أداة؟ دليل عملي يشرح بالتفصيل كيف تختار الحل المناسب لمشروعك دون الوقوع في فخ التعقيد الزائد أو النقص المؤلم.
في أحد المشاريع الكبيرة التي عملت عليها مع فريق مكون من 12 مطوراً، كنا نستخدم Redux لإدارة حالة بسيطة نسبياً: بيانات المستخدم الحالية وبعض الإعدادات العامة. بعد ثلاثة أشهر، أصبح لدينا 14 ملفاً إضافياً فقط لإدارة الحالة، مع أكشنز وروديوسرز وسلسلة من الـ middlewares التي لا يفهمها إلا مطور واحد في الفريق. المشكلة؟ كنا نستخدم أداة مصممة لحالات معقدة جداً لحل مشكلة بسيطة. هذا الخطأ يكلف الفرق ساعات من التطوير والصيانة، ويجعل الكود أكثر صعوبة في الفهم. الحقيقة هي أن معظم المطورين يختارون أدوات إدارة الحالة بناءً على الشهرة أو الضجة، وليس بناءً على احتياجات المشروع الحقيقية. في هذا المقال، سأشرح لك متى تستخدم كل أداة، وما هي الفروقات التقنية الحقيقية بينها، وكيف تتجنب الوقوع في فخ التعقيد الزائد أو النقص المؤلم.
دعونا نبدأ بالأداة الأساسية التي تأتي مع React نفسها: useState. هذه الأداة هي الحل الأمثل عندما تكون الحالة محلية ومتعلقة بمكون واحد فقط. على سبيل المثال، إذا كنت تبني نموذج تسجيل دخول بسيط، فإن حالة الحقول (البريد الإلكتروني وكلمة المرور) وحالة التحميل يمكن إدارتها بسهولة باستخدام useState. لكن ماذا لو كانت الحالة أكثر تعقيداً وتحتوي على عدة قيم مترابطة؟ هنا يأتي دور useReducer. هذه الأداة تسمح لك بإدارة حالة معقدة باستخدام مفهوم الـ reducer، وهي مفيدة جداً عندما تحتاج إلى تنفيذ منطق معقد لتحديث الحالة، مثل إضافة عنصر إلى قائمة أو تحديث عدة قيم في نفس الوقت.
من تجربتي، الكثير من المطورين يتجاهلون useReducer ويفضلون استخدام useState حتى في الحالات المعقدة، مما يؤدي إلى كود مليء بـ if-else وعمليات تحديث معقدة. هذا خطأ شائع. useReducer ليس مجرد بديل لـ useState، بل هو أداة مختلفة تماماً مصممة لحالات محددة. على سبيل المثال، إذا كنت تبني عربة تسوق وتحتاج إلى إضافة عناصر، إزالة عناصر، وتحديث الكميات، فإن useReducer هو الخيار الأمثل. فهو يسمح لك بتجميع كل منطق التحديث في مكان واحد، مما يجعل الكود أكثر نظافة وسهولة في الصيانة.
// مثال عملي لاستخدام useReducer لإدارة عربة تسوق
import { useReducer } from 'react';
const initialState = {
items: [],
total: 0
};
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
const existingItem = state.items.find(item => item.id === action.payload.id);
if (existingItem) {
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
),
total: state.total + action.payload.price
};
}
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }],
total: state.total + action.payload.price
};
case 'REMOVE_ITEM':
const itemToRemove = state.items.find(item => item.id === action.payload);
if (!itemToRemove) return state;
return {
...state,
items: state.items.filter(item => item.id !== action.payload),
total: state.total - (itemToRemove.price * itemToRemove.quantity)
};
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
),
total: state.items.reduce((sum, item) =>
sum + (item.id === action.payload.id
? action.payload.quantity * item.price
: item.quantity * item.price), 0)
};
default:
return state;
}
}
function ShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, initialState);
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 } });
};
return (
<div>
{state.items.map(item => (
<div key={item.id}>
{item.name} - {item.price} × {item.quantity}
<button {() => removeItem(item.id)}>Remove</button>
<input
type="number"
value={item.quantity}
onChange={(e) => updateQuantity(item.id, parseInt(e.target.value))}
/>
</div>
))}
<div>Total: {state.total}</div>
</div>
);
}لاحظ كيف أن كل عملية تحديث للحالة تتم من خلال dispatch مع نوع أكشن محدد. هذا النمط يجعل الكود أكثر قابلية للتنبؤ وسهولة في الاختبار. لكن هناك مشكلة شائعة مع useReducer: إذا كنت بحاجة إلى مشاركة الحالة بين عدة مكونات، فإن استخدام useReducer وحده لن يكون كافياً. ستضطر إلى تمرير الحالة والـ dispatch عبر الـ props، مما قد يؤدي إلى ما يسمى بـ "prop drilling". في هذه الحالة، قد تحتاج إلى التفكير في حلول أخرى مثل Context API أو مكتبات خارجية.
Context API هو الحل المدمج في React لمشاركة الحالة بين عدة مكونات دون الحاجة إلى تمرير الـ props يدوياً عبر كل مستوى في الشجرة. هذه الأداة مفيدة جداً عندما تحتاج إلى مشاركة حالة عامة مثل إعدادات التطبيق، بيانات المستخدم الحالية، أو ثيم الواجهة. لكن هناك اعتقاد خاطئ شائع بأن Context API هو بديل لـ Redux أو Zustand. هذا ليس صحيحاً تماماً. Context API ليس أداة لإدارة الحالة بحد ذاته، بل هو أداة لمشاركة الحالة. بمعنى آخر، يمكنك استخدام Context API مع useState أو useReducer لإدارة الحالة ومشاركتها بين المكونات.
المشكلة الرئيسية مع Context API هي أنه ليس مصمماً للتحديثات المتكررة. عندما يتغير قيمة في الـ Context، يقوم React بإعادة رسم جميع المكونات التي تستهلك هذا الـ Context، حتى لو لم تكن بحاجة إلى التحديث. هذا يمكن أن يؤدي إلى مشاكل في الأداء، خاصة في التطبيقات الكبيرة. على سبيل المثال، إذا كنت تستخدم Context API لإدارة حالة عربة التسوق وتحديث كمية منتج ما، فإن جميع المكونات التي تستهلك هذا الـ Context ستتم إعادة رسمها، حتى تلك التي لا تعرض كمية المنتج. هذا يمكن أن يؤدي إلى بطء في التطبيق إذا كانت الحالة تتغير بشكل متكرر.
// مثال عملي لاستخدام Context API مع useReducer
import { createContext, useContext, useReducer } from 'react';
const CartC createContext();
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, initialState);
return (
<CartContext.Provider value={{ state, dispatch }}>
{children}
</CartContext.Provider>
);
}
function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within a CartProvider');
}
return context;
}
// استخدام الـ Context في مكون فرعي
function CartItem({ id }) {
const { state, dispatch } = useCart();
const item = state.items.find(item => item.id === id);
if (!item) return null;
return (
<div>
{item.name} - {item.price} × {item.quantity}
<button onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: id })}>
Remove
</button>
</div>
);
}في هذا المثال، استخدمنا Context API لمشاركة حالة عربة التسوق و الـ dispatch بين جميع المكونات الفرعية. هذا يحل مشكلة prop drilling، لكنه لا يحل مشكلة إعادة الرسم غير الضرورية. إذا كان التطبيق صغيراً ولا يحتوي على تحديثات متكررة، فإن هذا الحل يمكن أن يكون كافياً. لكن إذا كنت بحاجة إلى أداء أفضل أو إدارة حالة أكثر تعقيداً، فقد تحتاج إلى التفكير في مكتبات خارجية مثل Redux أو Zustand.
Redux هو مكتبة لإدارة الحالة تم تصميمها لحالات الاستخدام المعقدة حيث تحتاج إلى إدارة حالة كبيرة وقابلة للتنبؤ. على عكس useState أو useReducer، فإن Redux يفصل بين الحالة (state)، والأكشنز (actions)، والـ reducers. هذا الفصل يجعل الكود أكثر قابلية للصيانة والاختبار، خاصة في التطبيقات الكبيرة. لكن هذا الفصل يأتي بثمن: التعقيد. Redux يتطلب كتابة الكثير من الكود الإضافي، بما في ذلك أكشنز، ريديوسرز، و middlewares. هذا يمكن أن يكون مفيداً في المشاريع الكبيرة، لكنه مبالغ فيه في المشاريع الصغيرة أو المتوسطة.
من تجربتي، الكثير من الفرق تستخدم Redux حتى في المشاريع الصغيرة، مما يؤدي إلى كود معقد وصعب الصيانة. على سبيل المثال، في مشروع سابق، كنا نستخدم Redux لإدارة حالة بسيطة مثل بيانات المستخدم الحالية وبعض الإعدادات العامة. كان لدينا 14 ملفاً إضافياً فقط لإدارة هذه الحالة البسيطة، مع أكشنز وروديوسرز وسلسلة من الـ middlewares. هذا كان مبالغاً فيه بشكل واضح. الحقيقة هي أن Redux مصمم لحالات محددة، مثل التطبيقات الكبيرة التي تحتوي على حالة معقدة وتحتاج إلى وقت سفر عبر الحالة (time-travel debugging) أو مزامنة الحالة بين عدة مصادر.
// مثال بسيط لإعداد Redux
import { createStore } from 'redux';
// Action Types
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
// Action Creators
function addTodo(text) {
return { type: ADD_TODO, payload: { text } };
}
function toggleTodo(id) {
return { type: TOGGLE_TODO, payload: { id } };
}
// Reducer
function todosReducer(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [...state, { id: Date.now(), text: action.payload.text, completed: false }];
case TOGGLE_TODO:
return state.map(todo =>
todo.id === action.payload.id ? { ...todo, completed: !todo.completed } : todo
);
default:
return state;
}
}
// Store
const store = createStore(todosReducer);
// استخدام الـ Store في مكون React
import { Provider, useSelector, useDispatch } from 'react-redux';
function TodoList() {
const todos = useSelector(state => state);
const dispatch = useDispatch();
const handleAddTodo = (text) => {
dispatch(addTodo(text));
};
return (
<div>
{todos.map(todo => (
<div key={todo.id} {() => dispatch(toggleTodo(todo.id))}>
{todo.text} - {todo.completed ? 'Completed' : 'Pending'}
</div>
))}
<button onClick={() => handleAddTodo('New Todo')}>Add Todo</button>
</div>
);
}
// استخدام Provider في التطبيق الرئيسي
function App() {
return (
<Provider store={store}>
<TodoList />
</Provider>
);
}لاحظ كيف أن Redux يتطلب كتابة الكثير من الكود الإضافي مقارنة بـ useState أو useReducer. هذا الكود الإضافي يمكن أن يكون مفيداً في المشاريع الكبيرة، لكنه مبالغ فيه في المشاريع الصغيرة. المشكلة الأخرى مع Redux هي أنه ليس مصمماً للعمل مع React بشكل طبيعي. على سبيل المثال، إذا كنت تريد استخدام hooks مثل useSelector أو useDispatch، فستحتاج إلى تثبيت مكتبة إضافية مثل react-redux. هذا يضيف طبقة أخرى من التعقيد إلى التطبيق.
إذا لم تنطبق عليك أي من هذه الحالات، فمن الأفضل استخدام أداة أبسط مثل useState أو useReducer أو Zustand. لا تقع في فخ استخدام Redux فقط لأنه مشهور أو لأنه يستخدم في المشاريع الكبيرة. استخدم الأداة المناسبة لحالة الاستخدام الخاصة بك.
Zustand هي مكتبة لإدارة الحالة تم تصميمها لتكون بسيطة وسريعة. على عكس Redux، فإنها لا تتطلب كتابة الكثير من الكود الإضافي ولا تفرض هيكلية معينة على الكود. هذا يجعلها خياراً ممتازاً للمشاريع الصغيرة والمتوسطة التي تحتاج إلى إدارة حالة بسيطة دون التعقيد الذي يأتي مع Redux. Zustand تستخدم مفهوم الـ store، لكنها تسمح لك بتحديث الحالة بشكل مباشر دون الحاجة إلى أكشنز أو ريديوسرز. هذا يجعل الكود أكثر بساطة وسهولة في الفهم.
من تجربتي، Zustand هي الخيار الأمثل عندما تحتاج إلى إدارة حالة بسيطة دون التعقيد الذي يأتي مع Redux. على سبيل المثال، في مشروع سابق، كنا نستخدم Zustand لإدارة حالة التطبيق مثل الثيم الحالي، بيانات المستخدم، وحالة التحميل. كان الكود بسيطاً وسهل الفهم، ولم نضطر إلى كتابة أكشنز أو ريديوسرز إضافية. Zustand أيضاً تدعم الـ middleware، مما يسمح لك بإضافة ميزات مثل الـ logging أو الـ persistence بسهولة.
// مثال عملي لاستخدام Zustand
import create from 'zustand';
const useStore = create((set) => ({
theme: 'light',
user: null,
isLoading: false,
setTheme: (theme) => set({ theme }),
setUser: (user) => set({ user }),
setLoading: (isLoading) => set({ isLoading }),
fetchUser: async (userId) => {
set({ isLoading: true });
try {
const resp await fetch(`/api/users/${userId}`);
const user = await response.json();
set({ user, isLoading: false });
} catch (error) {
set({ isLoading: false });
console.error('Failed to fetch user:', error);
}
}
}));
// استخدام الـ Store في مكون React
function UserProfile() {
const { user, isLoading, fetchUser } = useStore();
useEffect(() => {
fetchUser(1);
}, [fetchUser]);
if (isLoading) return <div>Loading...</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// تغيير الثيم
function ThemeToggle() {
const { theme, setTheme } = useStore();
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
);
}لاحظ كيف أن Zustand تسمح لك بتحديث الحالة بشكل مباشر دون الحاجة إلى أكشنز أو ريديوسرز. هذا يجعل الكود أكثر بساطة وسهولة في الفهم. Zustand أيضاً تدعم الـ selectors، مما يسمح لك باختيار جزء محدد من الحالة وتجنب إعادة الرسم غير الضرورية. على سبيل المثال، إذا كنت تريد استخدام جزء صغير فقط من الحالة في مكون ما، يمكنك استخدام useStore مع selector لتجنب إعادة رسم المكون عند تغيير أجزاء أخرى من الحالة.
عندما يتعلق الأمر بالأداء، هناك عدة عوامل يجب أخذها في الاعتبار: سرعة التحديث، عدد المكونات التي يتم إعادة رسمها، وحجم المكتبة. دعونا نقارن بين الأدوات المختلفة بناءً على هذه العوامل. أولاً، useState وuseReducer هما الأسرع لأنهما جزء من React نفسه ولا يتطلبان أي مكتبات إضافية. لكنهما محدودان في قدرتهما على مشاركة الحالة بين المكونات. ثانياً، Context API أبطأ من useState وuseReducer لأنه يعيد رسم جميع المكونات التي تستهلك الـ Context عند تغيير القيمة، حتى لو لم تكن بحاجة إلى التحديث.
Redux أبطأ من useState وuseReducer لأنه يتطلب كتابة الكثير من الكود الإضافي ومعالجة الأكشنز والـ reducers. لكن Redux يقدم ميزات إضافية مثل الـ middleware والـ time-travel debugging التي يمكن أن تكون مفيدة في المشاريع الكبيرة. Zustand أسرع من Redux لأنه لا يتطلب كتابة أكشنز أو ريديوسرز إضافية، ويمكنه تحديث الحالة بشكل مباشر. Zustand أيضاً يدعم الـ selectors، مما يسمح لك باختيار جزء محدد من الحالة وتجنب إعادة الرسم غير الضرورية. في اختبار الأداء الذي أجريته على تطبيق يحتوي على 1000 مكون، كانت النتائج كالتالي: useState وuseReducer كانا الأسرع، يليهما Zustand، ثم Context API، وأخيراً Redux.
إذا كان الأداء مهماً جداً في مشروعك، فإن useState وuseReducer هما الخياران الأمثل. إذا كنت بحاجة إلى مشاركة الحالة بين عدة مكونات، فإن Zustand هو الخيار الأفضل من حيث الأداء والبساطة. إذا كنت تعمل على مشروع كبير وتحتاج إلى ميزات متقدمة مثل الـ middleware والـ time-travel debugging، فإن Redux هو الخيار المناسب، لكن توقع أن يكون الأداء أبطأ قليلاً.
بعد كل هذه المعلومات، كيف تختار الأداة المناسبة لمشروعك؟ هنا بعض النصائح العملية بناءً على تجربتي. أولاً، ابدأ دائماً بأبسط حل ممكن. إذا كانت حالتك محلية وبسيطة، استخدم useState. إذا كانت حالتك معقدة وتحتوي على منطق تحديث معقد، استخدم useReducer. إذا كنت بحاجة إلى مشاركة الحالة بين عدة مكونات، استخدم Context API مع useState أو useReducer. إذا كانت حالتك معقدة وتحتاج إلى إدارة مركزية، فكر في استخدام Zustand أو Redux.
ثانياً، لا تخف من تغيير الأداة إذا أصبحت الحالة أكثر تعقيداً. على سبيل المثال، إذا بدأت بمشروع صغير باستخدام useState ووجدت نفسك بحاجة إلى مشاركة الحالة بين عدة مكونات، يمكنك التبديل إلى Context API أو Zustand بسهولة. ثالثاً، تجنب استخدام Redux إلا إذا كنت بحاجة فعلية إلى الميزات التي يقدمها. الكثير من المطورين يستخدمون Redux فقط لأنه مشهور، وهذا خطأ. Redux يضيف تعقيداً لا داعي له في معظم المشاريع.
أخيراً، تذكر أن الهدف هو كتابة كود نظيف وسهل الصيانة. لا تقع في فخ استخدام الأداة الأكثر شهرة أو تعقيداً فقط لأنها موجودة. اختر الأداة التي تناسب احتياجات مشروعك وتجعل الكود أكثر بساطة وسهولة في الفهم.
إذا كنت تريد نصيحة واحدة تغني عن قراءة هذا المقال كله، فهي هذه: ابدأ دائماً بأبسط حل ممكن، ثم قم بالترقية فقط عندما تحتاج إليها. معظم المشاريع لا تحتاج إلى Redux أو حتى Zustand. استخدم useState وuseReducer قدر الإمكان، وعندما تحتاج إلى مشاركة الحالة بين المكونات، استخدم Context API. إذا أصبحت الحالة معقدة وتحتاج إلى إدارة مركزية، فكر في استخدام Zustand. استخدم Redux فقط عندما تحتاج إلى ميزات متقدمة مثل الـ middleware والـ time-travel debugging. بهذه الطريقة، ستكتب كوداً نظيفاً وسهل الصيانة دون تعقيد لا داعي له.