من useState إلى Redux وZustand: متى تستخدم كل أداة؟ وكيف تختار دون أن تقع في فخ التعقيد؟ دليل عملي للمطورين الذين يريدون حلولاً فعالة لا نظريات جافة.
في يوم من الأيام، كنت أعمل على لوحة تحكم معقدة لشركة ناشئة في دبي، وكان الفريق يستخدم Redux لإدارة حالة بسيطة مثل فتح وإغلاق المودال. بعد شهرين، أصبح لدينا 12 reducer و7 middleware، وكل تغيير بسيط يتطلب تعديل 4 ملفات. المشكلة؟ كنا نستخدم مطرقة ثقيلة لقتل نملة. هذا المقال هو محاولة لإنقاذك من نفس الخطأ - ليس بشرح كل أداة على حدة، بل بتوضيح متى تستخدم ماذا بناءً على حجم المشكلة وليس حجم الأداة.
المشكلة الحقيقية في إدارة الحالة ليست في الأدوات نفسها، بل في عدم فهم متى نحتاجها أصلاً. معظم المطورين يبدأون بـ useState ثم يقفزون إلى Redux عند أول إشارة لمشكلة، دون أن يدركوا أن React نفسها توفر حلولاً متقدمة مثل useReducer وContext. الحقيقة هي أن 80% من مشاكل الحالة يمكن حلها بأدوات مدمجة في React، و15% تحتاج إلى مكتبة خارجية خفيفة مثل Zustand، و5% فقط تستحق تعقيد Redux. دعنا نكسر هذه النسبة ونفهم لماذا.
عندما تتحدث عن إدارة الحالة في React، فأنت تتحدث أساساً عن شيئين: الذاكرة (memory) والتحديثات (updates). كل أداة لإدارة الحالة هي في النهاية مجرد طريقة لتنظيم هذين الجانبين. useState مثلاً يخزن القيمة في ذاكرة مكون React ويعيد تشغيل الـ render عند تغييرها. لكن ماذا يحدث عندما تحتاج هذه القيمة في مكون بعيد؟ هنا تبدأ المشكلة الحقيقية.
لنأخذ مثالاً عملياً: لديك مكون Parent يحتوي على مكون Child يحتوي على مكون Grandchild. إذا أردت تمرير قيمة من Parent إلى Grandchild، فستضطر لتمريرها عبر props في كل مستوى. هذا ما يسمى بـ prop drilling، وهو ليس مشكلة في حد ذاته، بل هو إشارة إلى أن حالتك ليست محلية بما يكفي. المشكلة الأكبر تظهر عندما تحتاج هذه القيمة في مكون بعيد جداً، أو عندما تحتاج إلى تحديثها من مكون آخر. هنا يأتي دور أدوات إدارة الحالة - ليس لأنها أفضل من props، بل لأنها توفر طريقة لتخزين القيمة في مكان واحد والوصول إليها من أي مكان دون المرور بكل المكونات الوسيطة.
// مثال على prop drilling - لاحظ كيف نمرر القيمة عبر مكونات لا تحتاجها
function Parent() {
const [user, setUser] = useState(null);
return <Child user={user} setUser={setUser} />;
}
function Child({ user, setUser }) {
return <Grandchild user={user} setUser={setUser} />;
}
function Grandchild({ user, setUser }) {
// هنا فقط نحتاج القيمة، لكن اضطررنا لتمريرها عبر مكونين
return <div>{user?.name}</div>;
}قبل أن تفكر في أي مكتبة خارجية، اسأل نفسك: هل يمكن حل مشكلتي بـ useState أو useReducer؟ الإجابة غالباً نعم، إذا كانت حالتك محلية لمكون واحد أو لعدد قليل من المكونات المتجاورة. useState مثالية للحالات البسيطة مثل حقول الفورم، أو فتح وإغلاق المودال، أو أي حالة ثنائية (boolean). لكن عندما تبدأ حالتك في النمو وتتضمن منطق معقد، هنا يأتي دور useReducer.
useReducer هو نسخة متقدمة من useState تسمح لك بفصل منطق التحديث عن المكون نفسه. بدلاً من كتابة setState في كل مكان، تكتب reducer function واحدة تحتوي على كل منطق التحديث. هذا مفيد جداً عندما يكون لديك حالات متعددة مرتبطة ببعضها، أو عندما تحتاج إلى تحديثات معقدة تعتمد على الحالة الحالية. مثلاً، إذا كنت تبني عربة تسوق، فستحتاج إلى تحديث الكمية وحساب الإجمالي وإدارة الخصومات - كل هذه العمليات مرتبطة ببعضها ويمكن تنظيمها بسهولة في reducer واحد.
// مثال على useReducer لإدارة عربة تسوق معقدة
const initialState = {
items: [],
total: 0,
discount: 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 'APPLY_DISCOUNT':
return {
...state,
discount: action.payload,
total: state.total * (1 - action.payload / 100)
};
default:
return state;
}
}
function ShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, initialState);
// ... منطق المكون
}من تجربتي، useReducer هو الحل الأمثل عندما: 1. لديك حالة معقدة تتضمن أكثر من قيمة مرتبطة 2. تحتاج إلى منطق تحديث معقد يعتمد على الحالة الحالية 3. تريد فصل منطق التحديث عن مكونات العرض 4. حالتك لا تحتاج إلى مشاركة عبر مكونات بعيدة الميزة الكبيرة لـ useReducer هي أنه يجعل منطق التحديث أكثر قابلية للتنبؤ به (predictable) ويقلل من الأخطاء الناتجة عن تحديثات غير متوقعة. لكن تذكر: useReducer لا يحل مشكلة مشاركة الحالة عبر مكونات بعيدة - لهذا تحتاج إلى Context أو مكتبة خارجية.
Context API هو الحل المدمج في React لمشاركة الحالة عبر مكونات بعيدة دون prop drilling. الفكرة بسيطة: تخزن القيمة في Context، ثم يمكن لأي مكون داخل هذا Context الوصول إليها. لكن هناك سوء فهم شائع حول Context - كثير من المطورين يعتقدون أنه بديل لـ Redux، وهذا خطأ كبير. Context ليس أداة لإدارة الحالة، بل هو أداة لمشاركة الحالة. الفرق دقيق لكنه مهم جداً.
المشكلة الرئيسية في Context هي أنه يعيد تشغيل الـ render لكل المكونات التي تستهلكه عند تغيير القيمة، حتى لو لم تكن هذه المكونات بحاجة إلى التحديث. مثلاً، إذا كان لديك Context يحتوي على قيمة user، وكل المكونات في تطبيقك تستهلك هذا Context، فسيتم إعادة تشغيل كل المكونات عند تغيير user - حتى المكونات التي لا تعرض بيانات المستخدم. هذا يمكن أن يؤدي إلى مشاكل أداء كبيرة في التطبيقات الكبيرة.
// مثال على Context يؤدي إلى تحديثات غير ضرورية
const UserC createContext();
function App() {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{ user, setUser }}>
<Header /> // سيتم إعادة تشغيله عند تغيير user
<Sidebar /> // سيتم إعادة تشغيله أيضاً
<MainContent /> // سيتم إعادة تشغيله أيضاً
</UserContext.Provider>
);
}
function Header() {
const { user } = useContext(UserContext);
// هذا المكون يعرض فقط اسم المستخدم، لكنه سيتم إعادة تشغيله
// عند تغيير أي قيمة في Context حتى لو لم تكن متعلقة به
return <div>{user?.name}</div>;
}الحل لهذه المشكلة هو تقسيم Context إلى عدة Contexts أصغر، بحيث لا يحتوي كل Context إلا على القيم المرتبطة ببعضها. مثلاً، بدلاً من وضع كل حالة التطبيق في Context واحد، يمكنك إنشاء Context منفصل للمستخدم، وآخر للإعدادات، وآخر للبيانات. بهذه الطريقة، عند تغيير قيمة في Context معين، سيتم إعادة تشغيل المكونات التي تستهلك هذا Context فقط.
// تقسيم Context لتحسين الأداء
const UserC createContext();
const SettingsContext = createContext();
const DataContext = createContext();
function App() {
const [user, setUser] = useState(null);
const [settings, setSettings] = useState({ theme: 'light' });
const [data, setData] = useState([]);
return (
<UserContext.Provider value={{ user, setUser }}>
<SettingsContext.Provider value={{ settings, setSettings }}>
<DataContext.Provider value={{ data, setData }}>
<Header /> // يستخدم UserContext فقط
<Sidebar /> // يستخدم SettingsContext فقط
<MainContent /> // يستخدم DataContext فقط
</DataContext.Provider>
</SettingsContext.Provider>
</UserContext.Provider>
);
}من تجربتي، Context هو الحل الأمثل عندما: 1. تحتاج إلى مشاركة حالة بسيطة عبر مكونات بعيدة 2. هذه الحالة لا تتغير بشكل متكرر (مثل بيانات المستخدم أو الإعدادات) 3. لا تحتاج إلى منطق تحديث معقد 4. تريد تجنب تعقيد المكتبات الخارجية لكن إذا كانت حالتك تتغير بشكل متكرر أو تحتاج إلى منطق تحديث معقد، فستحتاج إلى مكتبة خارجية مثل Zustand أو Redux.
Zustand هي مكتبة لإدارة الحالة ظهرت كبديل خفيف لـ Redux، لكنها في الحقيقة تقدم نموذجاً مختلفاً تماماً. بدلاً من الاعتماد على actions وreducers، Zustand يسمح لك بإنشاء store يحتوي على الحالة والمنطق الذي يعدلها، ويمكن لأي مكون الوصول إلى هذا Store مباشرة. هذا النموذج يجعل إدارة الحالة أكثر بساطة ومرونة، خاصة في التطبيقات المتوسطة الحجم.
الميزة الكبيرة لـ Zustand هي أنه لا يفرض عليك هيكلية معينة. يمكنك إنشاء store واحد يحتوي على كل حالة التطبيق، أو عدة stores لكل جزء من التطبيق. كما أنه يدعم middleware مثل Redux، لكنه يأتي مع ميزات مدمجة مثل persistence (حفظ الحالة في localStorage) وdevtools. لكن الأهم هو أنه لا يسبب تحديثات غير ضرورية للمكونات - المكونات التي تستخدم Zustand لا يتم إعادة تشغيلها إلا عند تغيير القيم التي تستهلكها فقط.
// مثال على Zustand لإدارة عربة تسوق
import create from 'zustand';
const useCartStore = create((set) => ({
items: [],
total: 0,
addItem: (item) => set((state) => {
const existingItem = state.items.find(i => i.id === item.id);
if (existingItem) {
return {
items: state.items.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
),
total: state.total + item.price
};
}
return {
items: [...state.items, { ...item, quantity: 1 }],
total: state.total + item.price
};
}),
applyDiscount: (percentage) => set((state) => ({
total: state.total * (1 - percentage / 100)
}))
}));
function ShoppingCart() {
const { items, total, addItem } = useCartStore();
// ... منطق المكون
}لاحظ كيف أن Zustand يسمح لك بتعريف المنطق داخل الـ store نفسه، بدلاً من فصله في reducer منفصل. هذا يجعل الكود أكثر تركيزاً وأسهل في الصيانة. كما أن Zustand يدعم selectors، مما يعني أنه يمكنك اختيار جزء معين من الحالة لتستمع إليه، مما يقلل من تحديثات المكونات غير الضرورية.
// استخدام selectors في Zustand لتقليل التحديثات
function CartTotal() {
// هذا المكون سيتم إعادة تشغيله فقط عند تغيير total
const total = useCartStore(state => state.total);
return <div>Total: ${total}</div>;
}
function CartItems() {
// هذا المكون سيتم إعادة تشغيله فقط عند تغيير items
const items = useCartStore(state => state.items);
return (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}من تجربتي، Zustand هو الحل الأمثل عندما: 1. تحتاج إلى إدارة حالة معقدة عبر مكونات بعيدة 2. تريد تجنب تعقيد Redux ولكن تحتاج إلى ميزات متقدمة 3. حالتك تتطلب منطق تحديث معقد 4. تريد مكتبة خفيفة وسهلة الإعداد Zustand هو الخيار المفضل لدي في معظم المشاريع الجديدة، لأنه يوفر التوازن المثالي بين البساطة والقوة. لكن هناك حالات تحتاج فيها إلى شيء أكثر قوة - وهذا هو دور Redux.
Redux هي المكتبة الأكثر شهرة لإدارة الحالة في React، لكنها أيضاً الأكثر تعقيداً. كثير من المطورين يستخدمون Redux في مشاريع لا تحتاج إليه، فقط لأنهم اعتادوا عليه أو لأنهم سمعوا أنه "الحل الأفضل". الحقيقة هي أن Redux تستحق التعقيد فقط في حالات محددة جداً، وليس لكل مشروع.
Redux مبني على ثلاثة مبادئ أساسية: single source of truth (مصدر واحد للحقيقة)، state is read-only (الحالة للقراءة فقط)، وchanges are made with pure functions (التغييرات تتم بواسطة دوال خالصة). هذه المبادئ تجعل إدارة الحالة أكثر قابلية للتنبؤ، لكنها تأتي بثمن: الكثير من الكود الإضافي والتعقيد. مثلاً، لتغيير قيمة بسيطة في Redux، تحتاج إلى: 1. إنشاء action type 2. إنشاء action creator 3. إنشاء reducer يعالج هذا الaction 4. إرسال الaction باستخدام dispatch هذا الكثير من الخطوات لقيمة بسيطة، أليس كذلك؟
// مثال على تغيير قيمة بسيطة في Redux
// 1. تعريف action type
const INCREMENT = 'counter/increment';
// 2. تعريف action creator
const increment = () => ({ type: INCREMENT });
// 3. تعريف reducer
function counterReducer(state = 0, action) {
switch (action.type) {
case INCREMENT:
return state + 1;
default:
return state;
}
}
// 4. استخدام الaction في المكون
function Counter() {
const count = useSelector(state => state.count);
const dispatch = useDispatch();
return (
<div>
<button {() => dispatch(increment())}>Increment</button>
<div>Count: {count}</div>
</div>
);
}هذا التعقيد له مبرراته. Redux مصمم للتطبيقات الكبيرة والمعقدة التي تحتاج إلى: 1. تتبع كل تغيير في الحالة (time-travel debugging) 2. مشاركة الحالة بين عدة تطبيقات أو مكتبات 3. منطق تحديث معقد يمكن تقسيمه إلى middleware 4. إدارة حالة كبيرة جداً تتطلب تقسيمها إلى عدة reducers في شركة مثل Twitter أو Airbnb، حيث تحتاج إلى إدارة حالة معقدة جداً وتتبع كل تغيير، Redux هو الخيار الأمثل. لكن في معظم التطبيقات، هذا التعقيد ليس ضرورياً.
من تجربتي، Redux هو الحل الأمثل عندما: 1. لديك تطبيق كبير جداً مع حالة معقدة جداً 2. تحتاج إلى تتبع كل تغيير في الحالة (مثل التطبيقات المالية) 3. تحتاج إلى مشاركة الحالة بين عدة تطبيقات أو مكتبات 4. لديك فريق كبير يحتاج إلى هيكلية واضحة لإدارة الحالة إذا لم تنطبق عليك هذه الحالات، فأنت على الأرجح لا تحتاج إلى Redux. استخدم Zustand بدلاً منه - ستوفر الكثير من الوقت والتعقيد.
حتى بعد اختيار الأداة المناسبة، هناك مشاكل شائعة يقع فيها المطورون عند إدارة الحالة. دعنا نستعرض بعضها وكيف تتجنبها.
المشكلة الأكبر في إدارة الحالة هي التحديثات غير المتوقعة التي تؤدي إلى إعادة تشغيل المكونات دون داعٍ. مثلاً، إذا كان لديك حالة تحتوي على كائن، وقمت بتحديث خاصية واحدة فيه، فقد تعتقد أن المكونات التي تستهلك هذه الخاصية فقط هي التي ستعيد التشغيل. لكن في الحقيقة، React يقارن الكائنات بالـ reference، وليس بالقيمة. هذا يعني أنه إذا قمت بتحديث كائن باستخدام spread operator، فستحصل على كائن جديد، مما يؤدي إلى إعادة تشغيل كل المكونات التي تستهلك هذا الكائن.
// مشكلة التحديثات غير المتوقعة
const [user, setUser] = useState({ name: 'Ahmed', age: 30 });
// هذا سيؤدي إلى إعادة تشغيل المكونات التي تستهلك user.name وuser.age
setUser({ ...user, age: 31 });
// الحل: استخدم useMemo أو تقسيم الحالة
const [name, setName] = useState('Ahmed');
const [age, setAge] = useState(30);عندما تستخدم مكتبات مثل Redux أو Zustand، قد تواجه مشكلة الـ memory leaks إذا لم تنتبه إلى كيفية إدارة الاشتراكات (subscriptions). مثلاً، إذا كان لديك مكون يستمع إلى جزء من الحالة في Zustand، ثم يتم إلغاء تحميل هذا المكون دون إلغاء الاشتراك، فقد يؤدي ذلك إلى تسرب الذاكرة. الحل هو استخدام useEffect مع cleanup function لإلغاء الاشتراك عند إلغاء تحميل المكون.
// تجنب memory leaks في Zustand
useEffect(() => {
const unsubscribe = useCartStore.subscribe(
state => state.items,
items => {
console.log('Items changed:', items);
}
);
return () => unsubscribe(); // إلغاء الاشتراك عند إلغاء تحميل المكون
}, []);إذا كنت تستخدم middleware في Redux أو Zustand، فاحذر من الـ blocking calls التي قد تعطل الـ Event Loop. مثلاً، إذا كان لديك middleware يقوم بعملية I/O مثل قراءة ملف أو استدعاء API، وتنفذ هذه العملية بشكل متزامن (synchronous)، فستعطل واجهة المستخدم حتى تنتهي العملية. الحل هو استخدام العمليات غير المتزامنة (asynchronous) مثل async/await أو Promises.
// تجنب blocking calls في Redux middleware
const asyncMiddleware = store => next => action => {
if (action.type === 'FETCH_DATA') {
// لا تفعل هذا - سيعلق واجهة المستخدم
// const data = fs.readFileSync('data.json');
// افعل هذا بدلاً منه
fetch('https://api.example.com/data')
.then(resp> response.json())
.then(data => store.dispatch({ type: 'DATA_LOADED', payload: data }));
}
return next(action);
};بعد أكثر من عشر سنوات في تطوير تطبيقات React، هذه هي الخارطة التي أستخدمها لاختيار أداة إدارة الحالة المناسبة: 1. إذا كانت حالتك محلية لمكون واحد أو لعدد قليل من المكونات المتجاورة، استخدم useState أو useReducer. 2. إذا كانت حالتك تحتاج إلى مشاركة عبر مكونات بعيدة ولا تتغير بشكل متكرر، استخدم Context API مع تقسيم Context إلى عدة Contexts أصغر. 3. إذا كانت حالتك معقدة وتحتاج إلى منطق تحديث متقدم، استخدم Zustand - إنه الخيار الأمثل لمعظم التطبيقات المتوسطة الحجم. 4. إذا كانت حالتك كبيرة جداً وتحتاج إلى تتبع كل تغيير أو مشاركة بين عدة تطبيقات، استخدم Redux - لكن فقط إذا كنت متأكداً من أنك بحاجة إلى كل هذا التعقيد. القاعدة الذهبية: ابدأ بالأدوات الأبسط، ثم انتقل إلى الأدوات الأكثر تعقيداً فقط عندما تحتاج إليها. لا تستخدم مطرقة ثقيلة لقتل نملة، حتى لو كانت المطرقة تبدو رائعة.
الخطوة التالية: اختر مشروعاً صغيراً لديك، وحاول إعادة هيكلة إدارة حالته باستخدام الأداة المناسبة. ستندهش من مقدار الكود الذي يمكنك حذفه دون التأثير على الوظائف. إدارة الحالة ليست عن الأدوات، بل عن فهم المشكلة وحلها بأبسط طريقة ممكنة.