هل تستخدم useState لكل شيء؟ أم تفرط في Redux بلا داعٍ؟ اكتشف متى تختار Context، Redux، Zustand، أو حتى useReducer، مع تحليل عميق للأداء والذاكرة خلف الكواليس.
في أحد المشاريع الكبيرة التي عملت عليها العام الماضي، كان لدينا مكون واحد فقط يستخدم Redux لتخزين حالة بسيطة: زر تشغيل/إيقاف الصوت. نعم، زر واحد. وعندما فتحت ملفات المشروع، وجدت ٤ ملفات مختلفة و١٢٠ سطر كود فقط لإدارة هذا الزر. المشكلة؟ لم يكن المطور الذي كتبه يعرف متى يستخدم useState ومتى ينتقل إلى حلول أكثر تعقيداً. النتيجة؟ تطبيق بطيء في التحديثات، وذاكرة مستنزفة بلا داعٍ، وكل مرة نضغط على الزر كان React يعيد تصيير ٣٠ مكوناً آخر دون حاجة. هذا ليس خطأ في Redux بحد ذاته، بل في اختيار الأداة الخاطئة للمهمة.
إدارة الحالة في React ليست مجرد اختيار بين useState وRedux. إنها قرار هندسي يؤثر على أداء التطبيق، وصيانة الكود، وحتى تجربة المطورين الآخرين في الفريق. المشكلة الأكبر أن معظم المقالات تتحدث عن "كيف" تستخدم هذه الأدوات، لكن قليل منها يشرح "متى" تستخدمها. في هذا الدليل، سأفكك لك كل أداة من أدوات إدارة الحالة في React، وأشرح لك بالضبط ماذا يحدث خلف الكواليس في الذاكرة والمعالج عندما تستخدمها، ومتى يجب أن تختارها دون غيرها. لن نتحدث عن النظريات، بل عن تجارب حقيقية من مشاريع إنتاجية، وأرقام أداء، وأخطاء وقعت فيها فرق تطوير كبيرة.
useState هو أول ما نتعلمه في React، وغالباً ما يكون آخر ما نفكر فيه. لكن حتى هذه الأداة البسيطة لها تفاصيل تقنية مهمة. عندما تستدعي useState، React لا يخزن القيمة فقط في الذاكرة، بل ينشئ عقدة في شجرة الـ Fiber الخاصة بالمكون. هذه العقدة تحتوي على القيمة الحالية، والقيمة السابقة، والدالة المحدثة، ومرجع إلى المكون نفسه. المشكلة تبدأ عندما تستخدم useState في مكونات متداخلة بشكل كبير، أو عندما تعتمد على قيم سابقة لتحديث الحالة.
خذ مثلاً هذا الكود البسيط الذي يزيد عداداً عند الضغط على زر:
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
setCount(count + 1);
};
return <button {increment}>{count}</button>;
}إذا ظننت أن هذا الكود سيزيد العداد مرتين، فأنت مخطئ. النتيجة ستكون زيادة واحدة فقط. لماذا؟ لأن setCount لا يقوم بتحديث الحالة فوراً، بل يضيف تحديثاً إلى قائمة الانتظار في الـ Event Loop. وعندما تعالج React هذه التحديثات، تستخدم القيمة الأخيرة التي كانت متاحة عند بداية معالجة الحدث، وهي القيمة الأصلية لـ count. الحل؟ استخدام الدالة المحدثة:
const increment = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};هذا الكود سيزيد العداد مرتين بالفعل. لكن لاحظ كيف أن حتى أبسط أدوات إدارة الحالة تتطلب فهماً عميقاً لكيفية عمل React خلف الكواليس. useState ممتاز للحالات المحلية البسيطة، لكن عندما تبدأ الحالة في النمو، أو تحتاج إلى مشاركتها بين مكونات بعيدة، تبدأ المشاكل في الظهور. مثلاً، إذا كان لديك مكون رئيسي يحتوي على ١٠ مكونات فرعية، وكل منها يحتاج إلى جزء من الحالة، ستجد نفسك تمرر الـ props عبر ٥ مستويات من المكونات، وهذا ما يسمى "prop drilling"، وهو كابوس للصيانة.
عندما ظهر Context API في React 16.3، ظن الكثيرون أنه الحل السحري لكل مشاكل إدارة الحالة. الفكرة تبدو رائعة: تخزين الحالة في مكان مركزي، وجعلها متاحة لأي مكون دون الحاجة لتمرير الـ props. لكن الحقيقة أن Context ليس مصمماً لإدارة الحالة الديناميكية، بل للمعلومات الثابتة نسبياً مثل إعدادات الثيم، أو بيانات المستخدم، أو اللغة الحالية. المشكلة تبدأ عندما تستخدم Context لتخزين حالة تتغير بشكل متكرر، مثل بيانات نموذج أو فلتر بحث.
عندما يتغير قيمة في Context، React يعيد تصيير كل المكونات التي تستهلك هذا Context، حتى لو لم تكن تحتاج إلى القيمة التي تغيرت. هذا يعني أنه إذا كان لديك Context يحتوي على ١٠ قيم مختلفة، وتغيرت قيمة واحدة فقط، فسيتم إعادة تصيير كل المكونات التي تستخدم أي قيمة من هذه القيم. في مشروع عملت عليه، كان لدينا Context واحد يحتوي على حالة المستخدم، وإعدادات الثيم، وبيانات البحث، والفلاتر. النتيجة؟ عند تغيير فلتر واحد، كان React يعيد تصيير ٤٠ مكوناً على الصفحة، حتى تلك التي لا تتعلق بالفلتر على الإطلاق. الحل؟ تقسيم Context إلى عدة Contexts أصغر، كل منها مسؤول عن جزء محدد من الحالة.
// ❌ سيء: Context واحد ضخم
const AppC createContext();
// ✅ جيد: تقسيم Context حسب الغرض
const UserContext = createContext();
const ThemeContext = createContext();
const SearchContext = createContext();لكن حتى مع تقسيم Context، هناك مشكلة أخرى: الأداء. Context ليس مصمماً للتحديثات المتكررة. عندما يتغير قيمة في Context، React يستخدم خوارزمية مقارنة بسيطة لتحديد أي المكونات يجب إعادة تصييرها. هذه الخوارزمية لا تقوم بـ "shallow compare" مثل shouldComponentUpdate أو React.memo، بل تعتمد على مراجع الكائنات. هذا يعني أنه إذا قمت بتحديث قيمة في Context بكائن جديد، حتى لو كانت القيم داخله لم تتغير، فسيتم إعادة تصيير كل المكونات التي تستهلك هذا Context. في مشروع آخر، كان لدينا Context لتخزين بيانات جدول، وكان التحديث يتم كل ثانية. النتيجة؟ الصفحة كانت تتجمد كل بضع ثوانٍ لأن React كان يعيد تصيير ٢٠٠ صف في الجدول في كل تحديث.
useReducer هو الخطوة المنطقية التالية عندما يصبح useState معقداً جداً. الفكرة مشابهة لـ Redux، لكن بدون كل التعقيدات الإضافية. useReducer يمنحك طريقة منظمة لإدارة الحالة المعقدة التي تعتمد على عدة قيم أو منطق تحديث معقد. لكن لماذا لا نستخدم useState فقط؟ لأن useState يصبح صعب الصيانة عندما يكون لديك عدة قيم مرتبطة ببعضها، أو عندما يعتمد تحديث قيمة على القيم الأخرى.
خذ مثلاً نموذج تسجيل دخول يحتوي على حقول للاسم، والبريد الإلكتروني، وكلمة المرور، وتأكيد كلمة المرور. الحالة هنا ليست مجرد قيم مستقلة، بل هناك منطق معقد: يجب أن يكون البريد الإلكتروني بصيغة صحيحة، وكلمة المرور يجب أن تكون بطول معين، وتأكيد كلمة المرور يجب أن يطابق كلمة المرور الأصلية. مع useState، سيتحول الكود إلى مجموعة من الـ if-else المتداخلة، وسيكون من الصعب تتبع كيفية تحديث كل قيمة. مع useReducer، يمكنك تنظيم كل هذا المنطق في مكان واحد:
function formReducer(state, action) {
switch (action.type) {
case 'SET_NAME':
return { ...state, name: action.payload };
case 'SET_EMAIL':
return {
...state,
email: action.payload,
isEmailValid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(action.payload)
};
case 'SET_PASSWORD':
return {
...state,
password: action.payload,
isPasswordValid: action.payload.length >= 8,
isFormValid: action.payload.length >= 8 && state.isEmailValid
};
default:
return state;
}
}
function LoginForm() {
const [state, dispatch] = useReducer(formReducer, {
name: '',
email: '',
password: '',
isEmailValid: false,
isPasswordValid: false,
isFormValid: false
});
// ... بقية الكود
}لكن useReducer ليس حلاً سحرياً. المشكلة الأكبر هي أن useReducer لا يحل مشكلة مشاركة الحالة بين المكونات البعيدة. إذا كان لديك مكونين يحتاجان إلى نفس الحالة، فسيظل عليك تمرير الـ dispatch وstate عبر الـ props، أو استخدام Context. وهذا يقودنا إلى مشكلة أخرى: إذا استخدمت Context مع useReducer، فستواجه نفس مشاكل الأداء التي واجهناها مع Context العادي. الحل؟ استخدام مكتبات خارجية مثل Zustand أو Jotai، التي تقدم حلولاً أكثر كفاءة لمشاركة الحالة بين المكونات.
Redux هو أشهر مكتبة لإدارة الحالة في React، لكنه أيضاً أكثرها سوء فهم. الكثير من المطورين يستخدمون Redux لمجرد أنهم سمعوا أنه "المعيار الذهبي" لإدارة الحالة، دون أن يفهموا متى يحتاجونه حقاً. الحقيقة أن Redux مصمم لحالات محددة جداً: عندما يكون لديك حالة معقدة جداً، تحتاج إلى مشاركتها بين العديد من المكونات البعيدة، وتتطلب وقت سفر طويل (time-travel debugging)، أو تحتاج إلى معالجة جانبية معقدة مثل الـ middleware.
المشكلة الأكبر في Redux هي التعقيد. لإنشاء متجر Redux بسيط، تحتاج إلى كتابة عدة ملفات: ملف للأفعال (actions)، ملف للمخفضات (reducers)، ملف للمتجر (store)، وربما ملفات لـ selectors وmiddleware. هذا يعني أنه حتى لإدارة حالة بسيطة مثل زر تشغيل/إيقاف الصوت، ستحتاج إلى كتابة عشرات الأسطر من الكود. في أحد المشاريع، كان لدينا متجر Redux يحتوي على ١٥ reducer مختلف، وكل منها كان يحتوي على عشرات الـ actions. النتيجة؟ كان من المستحيل تتبع تدفق البيانات، وكل تغيير صغير كان يتطلب تعديل عدة ملفات. وعندما حاولنا تحسين الأداء باستخدام reselect، وجدنا أن الـ selectors كانت تعيد حساب القيم حتى عندما لا تكون هناك حاجة لذلك، لأننا لم نفهم تماماً كيفية عملها خلف الكواليس.
// مثال على تعقيد Redux حتى للحالات البسيطة
// actions.js
const TOGGLE_SOUND = 'TOGGLE_SOUND';
export const toggleSound = () => ({
type: TOGGLE_SOUND
});
// reducer.js
const initialState = { isSoundOn: false };
export default function soundReducer(state = initialState, action) {
switch (action.type) {
case TOGGLE_SOUND:
return { ...state, isSoundOn: !state.isSoundOn };
default:
return state;
}
}
// store.js
import { createStore } from 'redux';
import soundReducer from './reducer';
export const store = createStore(soundReducer);
// استخدام المتجر في المكون
import { useSelector, useDispatch } from 'react-redux';
function SoundButton() {
const isSoundOn = useSelector(state => state.isSoundOn);
const dispatch = useDispatch();
return (
<button {() => dispatch(toggleSound())}>
{isSoundOn ? 'إيقاف الصوت' : 'تشغيل الصوت'}
</button>
);
}لكن Redux ليس سيئاً دائماً. في المشاريع الكبيرة التي تحتاج إلى معالجة معقدة للبيانات، مثل تطبيقات التجارة الإلكترونية أو لوحات التحكم الإدارية، يمكن أن يكون Redux حلاً ممتازاً. المفتاح هو فهم متى تحتاج إلى كل هذه الميزات. مثلاً، إذا كنت تبني لوحة تحكم تحتوي على عشرات المكونات التي تحتاج إلى نفس البيانات، وتحتاج إلى تتبع تاريخ التغييرات، أو تحتاج إلى مزامنة الحالة بين عدة تبويبات في المتصفح، فقد يكون Redux هو الخيار الصحيح. لكن إذا كنت تبني تطبيقاً بسيطاً يحتوي على بضع صفحات، فقد يكون استخدام Redux مثل استخدام مطرقة لقتل ذبابة.
في السنوات الأخيرة، ظهرت مكتبات جديدة لإدارة الحالة في React تحاول حل مشاكل Redux وContext. أشهرها Zustand وJotai. هاتان المكتبتان تقدمان حلولاً أبسط وأكثر كفاءة لإدارة الحالة المشتركة بين المكونات، دون التعقيدات التي تأتي مع Redux أو مشاكل الأداء التي تأتي مع Context.
Zustand هي مكتبة صغيرة الحجم تقدم طريقة بسيطة لإنشاء متاجر حالة يمكن الوصول إليها من أي مكون. الفكرة مشابهة لـ Redux، لكن بدون الحاجة إلى actions أو reducers أو middleware. ببساطة، تنشئ متجراً يحتوي على الحالة والدوال المحدثة، ويمكن لأي مكون الوصول إلى هذا المتجر باستخدام خطاف مخصص. الميزة الأكبر لـ Zustand هي الأداء: عندما يتغير جزء من الحالة، فقط المكونات التي تستخدم هذا الجزء سيتم إعادة تصييرها. هذا يختلف عن Context، الذي يعيد تصيير كل المكونات التي تستخدم أي جزء من الحالة.
import create from 'zustand';
const useStore = create(set => ({
isSoundOn: false,
toggleSound: () => set(state => ({ isSoundOn: !state.isSoundOn })),
count: 0,
increment: () => set(state => ({ count: state.count + 1 }))
}));
function SoundButton() {
const isSoundOn = useStore(state => state.isSoundOn);
const toggleSound = useStore(state => state.toggleSound);
return (
<button {toggleSound}>
{isSoundOn ? 'إيقاف الصوت' : 'تشغيل الصوت'}
</button>
);
}
function Counter() {
const count = useStore(state => state.count);
const increment = useStore(state => state.increment);
return <button onClick={increment}>{count}</button>;
}لاحظ كيف أن Zustand يسمح لك بإنشاء متجر واحد يحتوي على عدة قيم وحالات مختلفة، ويمكن لكل مكون الوصول فقط إلى الجزء الذي يحتاجه. هذا يعني أنه إذا تغيرت قيمة count، فلن يتم إعادة تصيير SoundButton، والعكس صحيح. هذا حل فعال جداً لمشكلة الأداء التي نواجهها مع Context. بالإضافة إلى ذلك، Zustand يدعم الـ middleware، مما يعني أنه يمكنك إضافة ميزات مثل تسجيل التغييرات أو معالجة الأخطاء بسهولة.
أما Jotai، فهي مكتبة مختلفة تماماً في فلسفتها. بدلاً من إنشاء متجر مركزي واحد، Jotai تسمح لك بإنشاء "ذرات" (atoms) صغيرة من الحالة، يمكن دمجها معاً لإنشاء حالة معقدة. الفكرة مشابهة لـ Recoil من فيسبوك، لكنها أبسط وأكثر مرونة. الميزة الكبرى لـ Jotai هي أنها تسمح لك بإنشاء حالة مشتقة بسهولة، دون الحاجة إلى كتابة selectors معقدة كما في Redux.
import { atom, useAtom } from 'jotai';
const isSound atom(false);
const countAtom = atom(0);
const isSoundOnAndCountAtom = atom(get => {
const isSoundOn = get(isSoundOnAtom);
const count = get(countAtom);
return isSoundOn && count > 5;
});
function SoundButton() {
const [isSoundOn, setIsSoundOn] = useAtom(isSoundOnAtom);
return (
<button onClick={() => setIsSoundOn(!isSoundOn)}>
{isSoundOn ? 'إيقاف الصوت' : 'تشغيل الصوت'}
</button>
);
}
function Counter() {
const [count, setCount] = useAtom(countAtom);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
function Status() {
const isSoundOnAndCount = useAtomValue(isSoundOnAndCountAtom);
return <div>{isSoundOnAndCount ? 'الصوت مفعل والعداد كبير' : 'حالة أخرى'}</div>;
}الاختيار بين Zustand وJotai يعتمد على طبيعة المشروع. إذا كنت بحاجة إلى متجر مركزي واحد يحتوي على كل الحالة، فقد يكون Zustand هو الخيار الأفضل. أما إذا كنت بحاجة إلى حالة موزعة ومرنة، فقد تكون Jotai هي الحل الأمثل. في أحد المشاريع التي عملت عليها، استخدمنا Jotai لإدارة حالة معقدة جداً تحتوي على عشرات القيم المترابطة، وكان الأداء مذهلاً. المكونات كانت تعيد التصيير فقط عندما تتغير القيم التي تعتمد عليها، دون أي تحديثات غير ضرورية.
بعد كل هذه المعلومات، كيف تختار الأداة المناسبة لإدارة الحالة في مشروعك؟ هنا دليل عملي مبني على سنوات من الخبرة في مشاريع حقيقية:
هناك قاعدة ذهبية يجب أن تتبعها دائماً: ابدأ بالحل الأبسط، ثم انتقل إلى الحلول الأكثر تعقيداً فقط عندما تحتاج إليها. مثلاً، ابدأ بـ useState، وإذا وجدت أن الكود أصبح صعب الصيانة، انتقل إلى useReducer. وإذا وجدت أنك بحاجة إلى مشاركة الحالة بين المكونات، انتقل إلى Zustand أو Jotai. ولا تنتقل إلى Redux إلا إذا كنت حقاً بحاجة إلى كل ميزاته المتقدمة. في معظم المشاريع التي عملت عليها، كان Zustand هو الحل الأمثل، لأنه يقدم توازناً رائعاً بين البساطة والأداء والمرونة.
أخيراً، تذكر أن إدارة الحالة ليست مجرد اختيار أداة، بل هي قرار هندسي يؤثر على كل جوانب المشروع. الأداة التي تختارها ستؤثر على أداء التطبيق، وصيانة الكود، وسهولة إضافة ميزات جديدة. لذا، خذ وقتك في اختيار الأداة المناسبة، وفهم كيف تعمل خلف الكواليس، ولا تتبع الموضة فقط لأن الجميع يستخدمون أداة معينة.
إذا أخذت شيئاً واحداً من هذا المقال، فليكن هذا: لا تفرط في تعقيد إدارة الحالة في مشروعك. معظم المشاريع لا تحتاج إلى Redux، ومعظم المكونات لا تحتاج إلى Zustand. ابدأ بـ useState، واستخدم الأدوات الأكثر تعقيداً فقط عندما تواجه مشكلة حقيقية تتطلبها. وعندما تختار أداة لإدارة الحالة، اسأل نفسك هذه الأسئلة الثلاثة:
وأخيراً، تذكر أن الهدف من إدارة الحالة هو جعل الكود أسهل في الصيانة والفهم، وليس أصعب. إذا وجدت نفسك تكتب عشرات الأسطر من الكود لإدارة حالة بسيطة، فأنت تفعل شيئاً خاطئاً. في أحد المشاريع التي عملت عليها، استبدلنا Redux بـ Zustand، وقمنا بتقليل حجم الكود بنسبة ٦٠٪، وتحسين أداء التطبيق بشكل ملحوظ. الدرس المستفاد؟ البساطة دائماً أفضل من التعقيد، والأداء الحقيقي يأتي من فهم كيف تعمل الأدوات خلف الكواليس، وليس من استخدامها بشكل أعمى.