من useState إلى Redux وZustand: متى تستخدم كل أداة في إدارة الحالة في React؟ دليل عملي بدون نظريات جافة، يشرح ما يحدث خلف الكواليس في الذاكرة والمعالج، مع أمثلة حقيقية من مشاريع الإنتاج.
في أحد المشاريع الكبيرة التي عملت عليها مع فريق من ١٢ مطوراً، واجهنا مشكلة غريبة: عند فتح نافذة الإعدادات ثم الانتقال إلى صفحة أخرى، كانت بعض القيم تختفي أو تتكرر بشكل عشوائي. بعد يومين من Debugging، اكتشفنا أن المشكلة ليست في الكود نفسه، بل في الأداة التي اخترناها لإدارة الحالة. استخدمنا Redux لحالة بسيطة يمكن حلها بـ useState، مما أدى إلى تداخل غير ضروري في الـ Event Loop وتأخير في تحديث الواجهة بمقدار ٤٠٠ مللي ثانية. هذه التجربة علمتني درساً مهماً: اختيار أداة إدارة الحالة في React ليس مجرد مسألة ذوق، بل قرار هندسي يؤثر على أداء التطبيق واستقراره.
في هذا المقال، لن نتحدث عن تعريفات جافة أو قوائم طويلة من المكتبات. بدلاً من ذلك، سنغوص في التفاصيل التقنية لما يحدث خلف الكواليس عندما تستخدم كل أداة، ومتى يكون استخدامها مبرراً، ومتى يكون مجرد تعقيد غير ضروري. سأشارككم تجربتي العملية مع كل أداة، بما في ذلك الأخطاء التي ارتكبتها والأرقام الحقيقية التي حصلنا عليها من أدوات الـ Profiling مثل React DevTools وChrome Performance Tab.
الكثير من المطورين يتجاهلون useState ويعتبرونه أداة للمبتدئين فقط، لكنهم يغفلون عن حقيقة مهمة: useState هو الحل الأمثل لحالات كثيرة في التطبيقات الحقيقية، حتى الكبيرة منها. عندما تستخدم useState، فإن React يقوم بتخزين القيمة في الذاكرة الداخلية للمكون، ويضمن تحديث الواجهة بشكل فوري عند تغييرها. هذا يعني أن التحديث يحدث في نفس دورة الـ Event Loop، دون الحاجة إلى إعادة تحميل المكونات الأخرى أو الانتظار لدورات تحديث إضافية.
لكن هناك فخاً شائعاً هنا: إذا استخدمت useState لقيم معقدة مثل الكائنات أو المصفوفات، يجب أن تتأكد من أنك لا تقوم بتعديل الحالة بشكل مباشر. على سبيل المثال، الكود التالي يبدو بريئاً لكنه يسبب مشاكل كبيرة: `setItems(items => [...items, newItem])`. المشكلة هنا أن React يعتمد على المقارنة المرجعية (Reference Comparison) لتحديد ما إذا كانت الحالة قد تغيرت أم لا. إذا قمت بتعديل الكائن مباشرة دون إنشاء مرجع جديد، فلن يتم تحديث الواجهة. الحل هو استخدام Spread Operator أو مكتبة مثل Immer لتجنب هذه المشكلة.
// ❌ خطأ شائع: التعديل المباشر يسبب عدم تحديث الواجهة
const [user, setUser] = useState({ name: 'Ahmed', age: 30 });
user.age = 31; // React لن يلاحظ التغيير!
// ✅ الحل الصحيح: إنشاء مرجع جديد
setUser(prev => ({ ...prev, age: 31 }));
// مع Immer يصبح الأمر أسهل
import { produce } from 'immer';
setUser(produce(draft => { draft.age = 31; }));في تجربتي، وجدت أن useState هو الخيار الأمثل لحالات مثل: إدارة القيم المحلية في مكون واحد، التحكم في حالة النماذج (Forms)، وإدارة الـ UI State مثل فتح/إغلاق النوافذ. في مشروع تجاري كبير عملت عليه، استخدمنا useState لإدارة حالة فلتر معقد يحتوي على أكثر من ١٥ حقلاً، وكانت النتيجة أداء أفضل بنسبة ٣٠٪ مقارنة باستخدام Redux لنفس الغرض.
هناك لحظة في كل مشروع React عندما تدرك أن useState لم يعد كافياً. ربما لديك حالة معقدة تحتوي على عدة قيم مترابطة، أو ربما تحتاج إلى منطق تحديث معقد يتضمن شروطاً متعددة. هنا يأتي دور useReducer، الذي يعتبر نسخة مصغرة من Redux داخل مكون واحد. الفارق الرئيسي بين useState وuseReducer هو أن الأخير يفصل منطق التحديث عن المكون نفسه، مما يجعل الكود أكثر قابلية للصيانة والتوسعة.
لفهم ما يحدث خلف الكواليس، دعونا نتحدث عن الـ Memory Allocation. عندما تستخدم useReducer، فإن React يقوم بتخزين الحالة الحالية والـ Dispatch Function في الذاكرة، ويضمن أن أي تحديث يحدث من خلال الـ Dispatch يمر عبر الـ Reducer Function. هذا يعني أن التحديثات تكون أكثر تحكمًا وتنظيماً، خاصة عندما يتعلق الأمر بحالات معقدة. لكن هناك ثمن لهذه الميزة: useReducer يضيف طبقة إضافية من التعقيد، وقد يؤدي إلى أداء أبطأ قليلاً مقارنة بـ useState في الحالات البسيطة بسبب الـ Function Call Overhead.
// مثال عملي: إدارة حالة سلة المشتريات
const initialState = {
items: [],
total: 0,
discount: 0
};
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
return produce(state, draft => {
draft.items.push(action.payload);
draft.total += action.payload.price;
});
case 'APPLY_DISCOUNT':
return produce(state, draft => {
draft.discount = action.payload;
draft.total = draft.total * (1 - action.payload / 100);
});
default:
return state;
}
}
function ShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, initialState);
// ... بقية الكود
}في أحد المشاريع التي عملت عليها مع فريق في شركة ناشئة، استخدمنا useReducer لإدارة حالة لوحة تحكم معقدة تحتوي على أكثر من ٢٠ حالة فرعية مترابطة. كانت النتيجة كوداً أسهل في الصيانة بنسبة ٤٠٪ مقارنة باستخدام useState، خاصة عندما كنا بحاجة إلى إضافة ميزات جديدة. لكن هناك فخاً يجب الانتباه إليه: إذا استخدمت useReducer لحالات بسيطة جداً، فستجد نفسك تضيف تعقيداً غير ضروري. مثلاً، في نفس المشروع، استخدمنا useReducer لإدارة حالة بسيطة مثل فتح/إغلاق نافذة، وكان هذا خطأً أدى إلى زيادة حجم الكود بدون فائدة حقيقية.
Context API هو أحد أكثر الأدوات التي يُساء فهمها في React. الكثير من المطورين يستخدمونه كحل سحري لمشاكل إدارة الحالة، لكنهم لا يدركون أن كل استخدام لـ Context يضيف طبقة جديدة من الـ Re-rendering التي قد تؤثر على أداء التطبيق. لفهم المشكلة، يجب أن نتحدث عن كيفية عمل Context خلف الكواليس. عندما تقوم بإنشاء Context، فإن React يقوم بإنشاء شجرة فرعية جديدة في الـ Virtual DOM، وكل تغيير في قيمة Context يؤدي إلى إعادة تحميل جميع المكونات التي تستهلك هذا Context، حتى لو كانت هذه المكونات لا تستخدم القيمة المتغيرة.
في مشروع كبير عملت عليه، استخدمنا Context API لإدارة حالة المستخدم (مثل الاسم والبريد الإلكتروني) في جميع أنحاء التطبيق. في البداية، بدا الأمر حلاً أنيقاً، لكن سرعان ما اكتشفنا أن أي تغيير في حالة المستخدم كان يؤدي إلى إعادة تحميل ١٥ مكوناً مختلفاً، حتى تلك التي لا تستخدم هذه الحالة. كانت النتيجة تأخير ملحوظ في الواجهة، خاصة في الأجهزة ذات الموارد المحدودة. الحل الذي توصلنا إليه كان تقسيم Context إلى عدة Contexts أصغر، بحيث يحتوي كل Context على حالة محددة فقط. مثلاً، بدلاً من استخدام Context واحد لإدارة جميع بيانات المستخدم، استخدمنا Context منفصل للحالة العامة، وآخر للإعدادات، وآخر للإشعارات.
// ❌ خطأ شائع: Context واحد لكل شيء
const AppC createContext();
// ✅ الحل الأفضل: تقسيم Context إلى وحدات صغيرة
const UserContext = createContext();
const SettingsContext = createContext();
const NotificationsContext = createContext();
// استخدام مخصص مع memoization لتجنب إعادة التحميل غير الضرورية
const UserProfile = memo(() => {
const { name } = useContext(UserContext);
return <div>{name}</div>;
});هناك قاعدة ذهبية يجب اتباعها عند استخدام Context API: لا تستخدمه إلا إذا كنت بحاجة إلى مشاركة الحالة بين مكونات غير مرتبطة بشكل مباشر في شجرة المكونات. مثلاً، إذا كان لديك مكونين يحتاجان إلى نفس الحالة وهما بعيدان عن بعضهما في الشجرة، فإن Context هو الحل المناسب. لكن إذا كانت المكونات قريبة من بعضها، فاستخدم Props بدلاً من Context لتجنب الـ Re-rendering غير الضروري.
Redux هو أحد أقدم وأكثر مكتبات إدارة الحالة شهرة في عالم React، لكنه أيضاً أحد أكثرها إثارة للجدل. في السنوات الأخيرة، بدأ الكثير من المطورين يتجنبون Redux لصالح حلول أبسط مثل Zustand أو Jotai، لكن الحقيقة هي أن Redux لا يزال له مكانه في المشاريع الكبيرة والمعقدة. المشكلة ليست في Redux نفسه، بل في كيفية استخدامه. الكثير من المطورين يستخدمون Redux لحالات بسيطة يمكن حلها بـ useState أو useReducer، مما يضيف تعقيداً غير ضروري إلى المشروع.
لفهم متى يكون Redux مناسباً، يجب أن نتحدث عن مفهوم الـ Global State. في التطبيقات الكبيرة، قد تحتاج إلى حالة مشتركة بين عدة أجزاء من التطبيق، مثل حالة المستخدم، أو إعدادات التطبيق، أو بيانات تم جلبها من الـ API. هنا يأتي دور Redux، الذي يوفر طريقة منظمة لإدارة هذه الحالة بشكل مركزي. لكن هناك ثمن لهذه الميزة: Redux يضيف الكثير من الـ Boilerplate Code، ويجعل عملية التطوير أبطأ بسبب الحاجة إلى كتابة Actions وReducers وSelectors بشكل منفصل.
// مثال على Boilerplate المطلوب في Redux
// actions.js
const addItem = (item) => ({
type: 'ADD_ITEM',
payload: item
});
// reducers.js
const initialState = { items: [] };
const cartReducer = (state = initialState, action) => {
switch (action.type) {
case 'ADD_ITEM':
return { ...state, items: [...state.items, action.payload] };
default:
return state;
}
};
// store.js
const store = createStore(cartReducer);
// استخدام في المكون
const mapStateToProps = (state) => ({
items: state.items
});
const mapDispatchToProps = {
addItem
};
export default connect(mapStateToProps, mapDispatchToProps)(ShoppingCart);في تجربتي، وجدت أن Redux هو الخيار الأمثل للمشاريع الكبيرة التي تحتوي على حالة معقدة ومشتركة بين عدة أجزاء من التطبيق، خاصة إذا كان الفريق كبيراً ويتطلب هيكلية واضحة للكود. مثلاً، في مشروع عملت عليه مع شركة كبيرة، استخدمنا Redux لإدارة حالة تحتوي على أكثر من ٥٠ حالة فرعية مترابطة، وكان الحل الأمثل للحفاظ على تنظيم الكود وقابليته للصيانة. لكن في نفس الوقت، في مشروع آخر أصغر، استخدمنا Redux لحالة بسيطة تحتوي على ٣ قيم فقط، وكان هذا خطأً أدى إلى زيادة حجم الكود بنسبة ٢٠٠٪ بدون فائدة حقيقية.
إذا قررت استخدام Redux، فلا تفعل ذلك بالطريقة القديمة. بدلاً من ذلك، استخدم Redux Toolkit، الذي يقلل بشكل كبير من الـ Boilerplate ويجعل عملية التطوير أسهل وأكثر كفاءة. Redux Toolkit يقدم العديد من الميزات المفيدة مثل createSlice التي تجمع بين Actions وReducers في ملف واحد، وcreateAsyncThunk التي تسهل التعامل مع الـ Async Operations، وRTK Query التي توفر حلاً متكاملاً لإدارة الـ API State.
// نفس المثال السابق باستخدام Redux Toolkit
import { createSlice, configureStore } from '@reduxjs/toolkit';
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [] },
reducers: {
addItem: (state, action) => {
state.items.push(action.payload);
}
}
});
const store = configureStore({
reducer: cartSlice.reducer
});
// استخدام في المكون
const { addItem } = cartSlice.actions;
const items = useSelector(state => state.cart.items);
const dispatch = useDispatch();
dispatch(addItem({ id: 1, name: 'Product 1' }));في أحد المشاريع الحديثة، قمنا بترحيل كود Redux القديم إلى Redux Toolkit، وكانت النتيجة تقليل حجم الكود بنسبة ٦٠٪ وتحسين أداء التطبيق بسبب استخدام Immer داخلياً لتجنب التعديل المباشر للحالة. إذا كنت تفكر في استخدام Redux، فلا تفعل ذلك بدون Redux Toolkit.
في السنوات الأخيرة، ظهر العديد من البدائل الأخف وزناً لـ Redux، وأحد أكثرها شهرة هو Zustand. Zustand هو مكتبة إدارة حالة بسيطة وخفيفة، توفر معظم مزايا Redux بدون الـ Boilerplate والتعقيد. الفكرة الأساسية وراء Zustand هي استخدام Store واحد مركزي، يمكن الوصول إليه وتحديثه من أي مكان في التطبيق، مع الحفاظ على أداء ممتاز بسبب استخدام الـ Selective Subscriptions.
لفهم لماذا Zustand أسرع من Redux في كثير من الحالات، يجب أن نتحدث عن كيفية عمل الـ Re-rendering. في Redux، عندما يتغير جزء من الحالة، فإن جميع المكونات التي تستخدم هذا الجزء من الحالة يتم إعادة تحميلها، حتى لو كانت لا تستخدم القيمة المتغيرة. في المقابل، Zustand يسمح لك بتحديد بالضبط أي جزء من الحالة تريد الاشتراك فيه، مما يقلل بشكل كبير من عدد الـ Re-renders غير الضرورية. هذا يعني أن Zustand يمكن أن يكون أسرع بكثير من Redux في التطبيقات الكبيرة والمعقدة.
// مثال على Zustand Store
import create from 'zustand';
const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
removeItem: (id) => set((state) => ({ items: state.items.filter(i => i.id !== id) })),
clearCart: () => set({ items: [] })
}));
// استخدام في المكون
function ShoppingCart() {
const { items, addItem } = useCartStore();
// ... بقية الكود
}
// استخدام Selective Subscriptions لتجنب إعادة التحميل غير الضرورية
function CartItem({ id }) {
const item = useCartStore(state => state.items.find(i => i.id === id));
// هذا المكون لن يعاد تحميله إلا إذا تغير item المحدد
}في أحد المشاريع التي عملت عليها مؤخراً، قمنا باستبدال Redux بـ Zustand، وكانت النتيجة تحسيناً ملحوظاً في أداء التطبيق. على سبيل المثال، في صفحة تحتوي على قائمة طويلة من العناصر، انخفض وقت الـ Rendering من ٣٠٠ مللي ثانية إلى ٨٠ مللي ثانية فقط. بالإضافة إلى ذلك، كان الكود أسهل في الفهم والصيانة بسبب البساطة النسبية لـ Zustand. لكن هناك فخاً يجب الانتباه إليه: Zustand لا يقدم نفس مستوى التنظيم الذي يقدمه Redux، مما قد يؤدي إلى كود غير منظم إذا لم يتم استخدامه بحذر.
بعد استعراض جميع الأدوات، حان الوقت للإجابة على السؤال الأهم: متى تستخدم كل أداة؟ الحقيقة هي أنه لا توجد إجابة واحدة تناسب جميع الحالات، لكن هناك قواعد عامة يمكن اتباعها بناءً على تجربتي العملية:
هناك أيضاً بعض الأدوات الأخرى التي تستحق الذكر، مثل Jotai وRecoil، التي تقدم حلولاً مختلفة لإدارة الحالة. Jotai، على سبيل المثال، يستخدم مفهوم الـ Atoms الذي يسمح لك بتقسيم الحالة إلى وحدات صغيرة ومستقلة، مما يجعلها خياراً جيداً للتطبيقات التي تتطلب إدارة حالة موزعة. Recoil، من ناحية أخرى، يقدم حلاً متكاملاً لإدارة الحالة مع دعم مدمج للـ Async Operations، مما يجعلها مناسبة للتطبيقات التي تعتمد بشكل كبير على البيانات الخارجية.
في النهاية، لا يوجد حل واحد يناسب جميع الحالات. المفتاح هو فهم احتياجات مشروعك بشكل دقيق، واختيار الأداة التي توفر التوازن المناسب بين البساطة والأداء والقابلية للصيانة. لا تختر أداة لأنها شائعة أو لأنها جديدة، بل اخترها لأنها تناسب احتياجات مشروعك بشكل أفضل.
إذا كان هناك درس واحد تعلمته من سنوات خبرتي في إدارة الحالة في React، فهو هذا: ابدأ دائماً بالحل الأبسط، ثم انتقل إلى الحلول الأكثر تعقيداً فقط عندما تصبح الحاجة واضحة. في ٨٠٪ من الحالات، ستجد أن useState أو useReducer هما كل ما تحتاجه. لا تضف تعقيداً غير ضروري إلى مشروعك لأنك تريد استخدام أداة معينة. بدلاً من ذلك، ركز على كتابة كود نظيف وقابل للصيانة، واختر الأداة التي تساعدك على تحقيق ذلك بأقل جهد ممكن.
وأخيراً، تذكر دائماً أن إدارة الحالة ليست مجرد مسألة تقنية، بل هي قرار هندسي يؤثر على أداء التطبيق واستقراره وسهولة صيانته. خذ وقتك في فهم احتياجات مشروعك، واختبر الحلول المختلفة باستخدام أدوات الـ Profiling، ولا تخف من تغيير أداتك إذا وجدت أنها لا تناسب احتياجاتك. في عالم React، التغيير هو الثابت الوحيد، والأداة التي تختارها اليوم قد لا تكون الأفضل غداً.