هل تستخدم useState لكل شيء؟ أم تتخبط بين Redux وContext وZustand؟ هذا الدليل العملي يفكك لك إدارة الحالة في React من منظور هندسي بحت، مع أمثلة واقعية وحالات استخدام محددة لتختار الأداة المناسبة دون هدر وقت أو موارد.
في يوم من الأيام، كنت أعمل على لوحة تحكم معقدة لشركة ناشئة في مجال الفنتك. كل شيء كان يعمل بشكل مثالي على الورق: واجهة مستخدم سلسة، بيانات تظهر في الوقت الفعلي، وإشعارات فورية. لكن بعد أسبوعين من الإطلاق، بدأت المشاكل تظهر. الصفحة تستغرق ٥ ثوانٍ لتحميل البيانات الأولية، والـ Memory Usage يقفز إلى ٨٠٠ ميجابايت عند فتح أكثر من ثلاث علامات تبويب، والمطورون الجدد يضيعون ساعات في تتبع تدفق البيانات بين المكونات. المشكلة؟ إدارة الحالة كانت فوضى: useState متداخلة في كل مكان، Context تُعاد إنشاؤها مع كل تغيير بسيط، وRedux تُستخدم حتى لتخزين حالة زر صغير. هذا المقال هو ما كنت أتمنى أن أجده قبل أن أبدأ المشروع.
إدارة الحالة في React ليست مجرد اختيار بين useState وRedux. إنها قرار هندسي يؤثر على أداء التطبيق، وصيانة الكود، وسهولة التوسع. المشكلة الأكبر التي أراها في المشاريع العربية والعالمية على حد سواء هي استخدام الأدوات بطريقة عشوائية: إما الإفراط في التعقيد باستخدام Redux لحالات بسيطة، أو الإفراط في التبسيط باستخدام useState لحالات تحتاج إلى إدارة مركزية. في هذا الدليل، سأفكك لك كل أداة من أدوات إدارة الحالة في React، متى تستخدمها، وماذا يحدث خلف الكواليس في الذاكرة والمعالج، مع أمثلة واقعية من مشاريع حقيقية.
useState هو أول ما نتعلمه في React، وغالباً ما يكون آخر ما نفكر فيه عند إدارة الحالة. لكن قلة من المطورين يفهمون حقاً كيف يعمل خلف الكواليس. عندما تستدعي useState، React لا تخزن القيمة ببساطة في متغير عادي. بدلاً من ذلك، تنشئ ما يسمى بـ "Fiber Node" في شجرة المكونات، وتربط هذه العقدة بمكان محدد في الذاكرة. هذا يعني أن كل تغيير في الحالة يؤدي إلى إعادة حساب لـ "Reconciliation" للمكون، وإذا كان لديك مكونات متداخلة بشكل عميق، قد يؤدي ذلك إلى إعادة رسم غير ضرورية للـ DOM.
المشكلة الأكبر مع useState ليست في الأداء، بل في التنظيم. عندما يكون لديك أكثر من ٥ أو ٦ حالات داخل مكون واحد، يصبح الكود غير قابل للصيانة. مثلاً، في مشروع سابق لشركة سعودية في مجال التجارة الإلكترونية، كان لدينا مكون ProductCard يحتوي على أكثر من ١٢ حالة مختلفة: isHovered, isExpanded, quantity, selectedColor, selectedSize, isInWishlist، وغيرها. النتيجة؟ مكون بطيء جداً، وصعب التعديل، ومليء بالـ Bugs عند تحديث أي حالة. القاعدة الذهبية هنا: إذا كان لديك أكثر من ٣ حالات داخل مكون واحد، فكر في تقسيم المكون أو استخدام أداة أخرى لإدارة الحالة.
// مثال سيئ: useState متداخلة بشكل مفرط
function ProductCard({ product }) {
const [isHovered, setIsHovered] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const [quantity, setQuantity] = useState(1);
const [selectedColor, setSelectedColor] = useState(null);
const [selectedSize, setSelectedSize] = useState(null);
const [isInWishlist, setIsInWishlist] = useState(false);
const [isAddingToCart, setIsAddingToCart] = useState(false);
const [error, setError] = useState(null);
// 50 سطر من المنطق المعقد هنا
return <div>{/* ... */}</div>;
}
// الحل الأفضل: تقسيم الحالة إلى كائنات أو استخدام useReducer
function ProductCard({ product }) {
const [state, dispatch] = useReducer(productReducer, {
isHovered: false,
isExpanded: false,
quantity: 1,
selectedColor: null,
selectedSize: null,
isInWishlist: false,
isAddingToCart: false,
error: null
});
// استخدام dispatch لتحديث الحالة
const handleAddToCart = () => {
dispatch({ type: 'ADD_TO_CART', payload: { productId: product.id } });
};
return <div>{/* ... */}</div>;
}
function productReducer(state, action) {
switch (action.type) {
case 'ADD_TO_CART':
return { ...state, isAddingToCart: true };
case 'UPDATE_QUANTITY':
return { ...state, quantity: action.payload };
// حالات أخرى
default:
return state;
}
}الكثير من المطورين يعتقدون أن useReducer هو مجرد بديل لـ Redux داخل المكون، لكن هذا فهم خاطئ. useReducer هو أداة قوية لإدارة الحالة المحلية عندما يكون لديك منطق معقد أو حالات مترابطة. الفرق الأساسي بين useState وuseReducer هو أن الأول مصمم للحالات البسيطة والقيم البدائية، بينما الثاني مصمم للحالات المركبة والمنطق المعقد.
خلف الكواليس، useReducer يعمل بطريقة مشابهة جداً لـ Redux، لكنه محصور داخل مكون واحد. عندما تستدعي dispatch، React تمرر الـ Action إلى الـ Reducer الذي تعريفه، ثم تقوم بتحديث الحالة بناءً على القيمة المرجعة. الميزة الكبيرة هنا هي أنك تفصل منطق التحديث عن منطق العرض، مما يجعل الكود أكثر قابلية للاختبار والصيانة. مثلاً، في تطبيق للمحاسبة كنت أعمل عليه، كان لدينا مكون InvoiceForm يحتوي على أكثر من ٢٠ حقل إدخال مترابط: إذا غير المستخدم الكمية، يجب تحديث السعر الإجمالي والضريبة والخصم. استخدام useReducer هنا كان الحل الأمثل، حيث يمكننا كتابة منطق مركزي لجميع التحديثات في مكان واحد.
// مثال عملي: نموذج فاتورة معقدة
function InvoiceForm() {
const [state, dispatch] = useReducer(invoiceReducer, {
customer: '',
items: [],
taxRate: 0.15,
discount: 0,
notes: '',
isSubmitting: false,
error: null
});
const handleAddItem = () => {
dispatch({ type: 'ADD_ITEM', payload: { name: '', quantity: 1, price: 0 } });
};
const handleItemChange = (index, field, value) => {
dispatch({
type: 'UPDATE_ITEM',
payload: { index, field, value }
});
};
// حساب الإجمالي تلقائياً عند تغيير أي حقل
const subtotal = state.items.reduce((sum, item) => sum + (item.quantity * item.price), 0);
const tax = subtotal * state.taxRate;
const total = subtotal + tax - state.discount;
return (
<form>
{/* حقول النموذج */}
<button type="button" {handleAddItem}>إضافة عنصر</button>
<div>الإجمالي: {total.toFixed(2)}</div>
</form>
);
}
function invoiceReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
return { ...state, items: [...state.items, action.payload] };
case 'UPDATE_ITEM':
const newItems = [...state.items];
newItems[action.payload.index] = {
...newItems[action.payload.index],
[action.payload.field]: action.payload.value
};
return { ...state, items: newItems };
case 'UPDATE_TAX_RATE':
return { ...state, taxRate: action.payload };
case 'UPDATE_DISCOUNT':
return { ...state, discount: action.payload };
case 'SUBMIT_START':
return { ...state, isSubmitting: true, error: null };
case 'SUBMIT_SUCCESS':
return { ...state, isSubmitting: false };
case 'SUBMIT_ERROR':
return { ...state, isSubmitting: false, error: action.payload };
default:
return state;
}
}رغم قوة useReducer، هناك حالات يجب فيها تجنبها. أولاً، إذا كانت حالتك بسيطة ولا تحتوي على منطق معقد، فإن useState سيكون خياراً أفضل وأبسط. ثانياً، إذا كنت بحاجة إلى مشاركة الحالة بين مكونات غير مرتبطة بشكل مباشر، فإن useReducer لن يكون الحل الأمثل، وستحتاج إلى Context أو مكتبة خارجية. ثالثاً، إذا كانت حالتك تحتاج إلى تحديثات متكررة جداً (مثل عداد في الثانية)، فإن useReducer قد يؤدي إلى إعادة رسم غير ضرورية للمكونات، وفي هذه الحالة قد يكون useRef أو مكتبة مثل Zustand أفضل.
Context API هي واحدة من أكثر الأدوات سوء فهم في React. الكثير من المطورين يستخدمونها كحل سحري لمشاركة البيانات بين المكونات، دون أن يدركوا أن كل تغيير في Context يؤدي إلى إعادة رسم لجميع المكونات التي تستهلك هذا Context، حتى لو لم تستخدم القيمة المتغيرة. هذا يعني أنه إذا كان لديك Context يحتوي على حالة المستخدم ومعلومات الثيم والبيانات العامة، فإن تغيير أي جزء منها سيؤدي إلى إعادة رسم جميع المكونات التي تستهلك هذا Context، مما قد يسبب مشاكل أداء كبيرة.
في مشروع لشركة إماراتية في مجال الصحة الرقمية، كان لدينا Context واحد ضخم يحتوي على أكثر من ٢٠ قيمة مختلفة، من بيانات المستخدم إلى إعدادات اللغة إلى حالة التحميل. النتيجة؟ كل مرة يقوم المستخدم بتغيير لغته، كانت جميع المكونات في التطبيق تعيد الرسم، بما في ذلك مكونات لا علاقة لها باللغة. الحل؟ تقسيم Context إلى عدة Contexts أصغر، كل منها مسؤول عن جزء محدد من الحالة. مثلاً، فصل Context المستخدم عن Context الثيم عن Context اللغة. بهذه الطريقة، عندما يتغير شيء في Context اللغة، فقط المكونات التي تستخدم اللغة هي التي تعيد الرسم.
// مثال سيئ: Context واحد ضخم
const AppC createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [language, setLanguage] = useState('ar');
const [isLoading, setIsLoading] = useState(false);
const [notifications, setNotifications] = useState([]);
const [cartItems, setCartItems] = useState([]);
// 15 قيمة أخرى...
return (
<AppContext.Provider value={{ user, setUser, theme, setTheme, language, setLanguage, isLoading, setIsLoading, notifications, setNotifications, cartItems, setCartItems }}>
{children}
</AppContext.Provider>
);
}
// الحل الأفضل: تقسيم Context إلى عدة Contexts أصغر
const UserContext = createContext();
const ThemeContext = createContext();
const LanguageContext = createContext();
const NotificationsContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [language, setLanguage] = useState('ar');
const [notifications, setNotifications] = useState([]);
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
<LanguageContext.Provider value={{ language, setLanguage }}>
<NotificationsContext.Provider value={{ notifications, setNotifications }}>
{children}
</NotificationsContext.Provider>
</LanguageContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}Context هو الحل الأمثل عندما تحتاج إلى مشاركة بيانات بين مكونات غير مرتبطة بشكل مباشر، ولا تريد تمرير الـ Props عبر عدة مستويات (ما يسمى بـ "Prop Drilling"). مثلاً، معلومات المستخدم الحالية، إعدادات الثيم، أو اللغة المختارة هي حالات مثالية لاستخدام Context. لكن حتى في هذه الحالات، يجب أن تكون حذراً. إذا كانت البيانات تتغير بشكل متكرر (مثل حالة التحميل أثناء جلب البيانات)، فقد يكون من الأفضل استخدام مكتبة خارجية مثل Zustand أو Jotai، التي توفر آليات لتحسين الأداء تمنع إعادة الرسم غير الضرورية.
Redux هو واحد من أكثر مكتبات إدارة الحالة إثارة للجدل في مجتمع React. البعض يعتبرها الحل النهائي لكل مشاكل إدارة الحالة، بينما يرى آخرون أنها تضيف تعقيداً غير ضروري. الحقيقة هي أن Redux لها مكانها، لكنها ليست الحل الأمثل لكل مشروع. خلف الكواليس، Redux تعمل بطريقة مختلفة تماماً عن useState أو useReducer. بدلاً من تخزين الحالة داخل مكونات React، Redux تخزنها في متجر خارجي، وتوفر آليات لتحديث هذه الحالة بشكل متحكم فيه باستخدام Actions وReducers.
المشكلة الأكبر مع Redux ليست في الأداء، بل في التعقيد. لكي تستخدم Redux بشكل صحيح، تحتاج إلى كتابة الكثير من الكود المتكرر: Actions، Action Creators، Reducers، Selectors، وأحياناً Middleware. هذا يعني أنه إذا كنت تعمل على مشروع صغير أو متوسط الحجم، فإن استخدام Redux قد يضيف عبئاً غير ضروري على فريق التطوير. مثلاً، في مشروع لشركة مصرية في مجال التعليم الإلكتروني، كان لدينا تطبيق يحتوي على حوالي ٣٠ مكون فقط، ومع ذلك استخدمنا Redux لإدارة كل شيء. النتيجة؟ فريق التطوير قضى وقتاً أطول في كتابة الـ Boilerplate أكثر من الوقت الذي قضاه في كتابة منطق العمل الفعلي. بعد مراجعة المشروع، استبدلنا Redux بـ Zustand، مما قلل حجم الكود بنسبة ٤٠٪ وزاد سرعة التطوير بشكل ملحوظ.
// مثال على تعقيد Redux: كود متكرر وممل
// actions.js
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
export const addTodo = (text) => ({
type: ADD_TODO,
payload: text
});
export const toggleTodo = (id) => ({
type: TOGGLE_TODO,
payload: id
});
// reducers.js
const initialState = {
todos: []
};
export default function todoReducer(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [...state.todos, { id: Date.now(), text: action.payload, completed: false }]
};
case TOGGLE_TODO:
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
)
};
default:
return state;
}
}
// store.js
import { createStore } from 'redux';
import todoReducer from './reducers';
export const store = createStore(todoReducer);
// استخدام في المكون
import { useSelector, useDispatch } from 'react-redux';
import { addTodo } from './actions';
function TodoList() {
const todos = useSelector(state => state.todos);
const dispatch = useDispatch();
const handleAddTodo = () => {
dispatch(addTodo('مهمة جديدة'));
};
return (
<div>
<button {handleAddTodo}>إضافة مهمة</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}Redux يصبح ضرورياً عندما يكون لديك تطبيق كبير ومعقد يحتوي على العديد من المكونات التي تحتاج إلى الوصول إلى نفس الحالة، وتحتاج إلى تتبع تغييرات الحالة بمرور الوقت (مثلاً، للتراجع عن الإجراءات أو إعادة تشغيلها). مثلاً، في تطبيقات إدارة المشاريع مثل Trello أو Jira، حيث تحتاج إلى إدارة حالة معقدة جداً تحتوي على لوحات ومهام ومستخدمين وتعليقات، فإن Redux يمكن أن يكون حلاً مناسباً. أيضاً، إذا كنت بحاجة إلى دمج Middleware مثل Redux-Saga أو Redux-Thunk للتعامل مع العمليات غير المتزامنة، فإن Redux يوفر بنية قوية لذلك.
لكن حتى في هذه الحالات، يجب أن تفكر جيداً قبل استخدام Redux. في السنوات الأخيرة، ظهرت مكتبات جديدة مثل Zustand وJotai تقدم حلولاً أبسط وأكثر كفاءة لإدارة الحالة في التطبيقات الكبيرة. مثلاً، Zustand تستخدم نفس مفهوم المتجر الخارجي الذي تستخدمه Redux، لكنها تفعل ذلك بطريقة أبسط بكثير، دون الحاجة إلى Actions وReducers وBoilerplate المزعج.
Zustand هي مكتبة إدارة حالة حديثة أصبحت الخيار المفضل للكثير من المطورين الذين يريدون تجنب تعقيد Redux دون التضحية بالقدرات. الفرق الأساسي بين Zustand وRedux هو أن Zustand تستخدم مفهوم الـ "Store" بطريقة أبسط وأكثر مرونة. بدلاً من كتابة Actions وReducers، يمكنك ببساطة إنشاء متجر وتحديث الحالة مباشرة باستخدام دوال بسيطة. خلف الكواليس، Zustand تستخدم نفس آليات React لتحسين الأداء، مثل استخدام الـ Selectors لمنع إعادة الرسم غير الضرورية.
في مشروع لشركة قطرية في مجال التجارة الإلكترونية، كنا نستخدم Redux لإدارة حالة عربة التسوق والمفضلة والإعدادات العامة. بعد مراجعة الأداء، وجدنا أن التطبيق يعاني من إعادة رسم غير ضرورية للمكونات عند تغيير أي جزء من الحالة. انتقلنا إلى Zustand، وكتبنا نفس المنطق في ثلث الكود تقريباً، مع تحسن ملحوظ في الأداء. مثلاً، مكون ProductCard الذي كان يعيد الرسم عند تغيير حالة المفضلة حتى لو لم تكن المفضلة جزءاً من هذا المكون، توقف عن إعادة الرسم بعد استخدام Zustand مع Selectors المناسبة.
// مثال عملي على Zustand: إدارة عربة التسوق والمفضلة
import create from 'zustand';
const useStore = create((set) => ({
cart: [],
wishlist: [],
addToCart: (product) => set((state) => ({
cart: [...state.cart, product]
})),
removeFromCart: (productId) => set((state) => ({
cart: state.cart.filter(item => item.id !== productId)
})),
addToWishlist: (product) => set((state) => ({
wishlist: [...state.wishlist, product]
})),
removeFromWishlist: (productId) => set((state) => ({
wishlist: state.wishlist.filter(item => item.id !== productId)
})),
clearCart: () => set({ cart: [] })
}));
// استخدام في المكون
function ProductCard({ product }) {
const { addToCart, addToWishlist, wishlist } = useStore();
const isInWishlist = wishlist.some(item => item.id === product.id);
return (
<div>
<h3>{product.name}</h3>
<button {() => addToCart(product)}>إضافة إلى السلة</button>
<button onClick={() => addToWishlist(product)}>
{isInWishlist ? 'إزالة من المفضلة' : 'إضافة إلى المفضلة'}
</button>
</div>
);
}
// استخدام Selectors لتحسين الأداء
function CartCount() {
const count = useStore(state => state.cart.length);
return <div>عدد المنتجات في السلة: {count}</div>;
}بالطبع، Zustand ليست مثالية لكل الحالات. إذا كنت بحاجة إلى ميزات متقدمة مثل Time Travel Debugging أو Middleware معقدة، فقد يكون Redux خياراً أفضل. لكن في معظم المشاريع، Zustand توفر كل ما تحتاجه لإدارة الحالة بطريقة بسيطة وفعالة.
بعد أكثر من عشر سنوات في تطوير تطبيقات React، هذه هي الخارطة التي أستخدمها شخصياً لاختيار أداة إدارة الحالة المناسبة:
القاعدة الذهبية التي أتبعها دائماً: ابدأ بالحل الأبسط، ثم انتقل إلى الحلول الأكثر تعقيداً فقط عندما تحتاج إليها. معظم المشاكل التي أراها في المشاريع ليست بسبب اختيار الأداة الخاطئة، بل بسبب استخدام أدوات معقدة لحالات بسيطة. مثلاً، لا تستخدم Redux لإدارة حالة زر صغير، ولا تستخدم Context لمشاركة بيانات تتغير كل ثانية. فكر في أداء التطبيق وصيانة الكود قبل أن تختار الأداة المناسبة.
وأخيراً، تذكر أن إدارة الحالة ليست مجرد اختيار أداة، بل هي تصميم بنية التطبيق. إذا وجدت نفسك تضيف الكثير من المكتبات والأدوات لإدارة الحالة، فقد يكون الوقت مناسباً لإعادة التفكير في بنية التطبيق بالكامل. أحياناً، الحل ليس في إضافة أداة جديدة، بل في تبسيط التصميم الحالي.