هل تستخدم useState لكل شيء؟ أو ربما Redux لكل مشكلة؟ اكتشف متى تستخدم Context، Zustand، أو حتى Signals، وكيف تتجنب الكوارث في الأداء والذاكرة عند إدارة الحالة في React.
في عام ٢٠٢٣، تلقينا مكالمة طوارئ من فريق تطوير في شركة ناشئة: تطبيقهم الذي يستخدمه ٥٠ ألف مستخدم يومياً أصبح بطيئاً لدرجة أن المستخدمين بدأوا يغلقونه بعد ٣٠ ثانية. السبب؟ ٢٧ مكون يستخدمون نفس الحالة عبر Context، وكل تغيير بسيط يؤدي إلى إعادة رسم ١٤٠ مكوناً دون داعٍ. المشكلة لم تكن في الكود، بل في اختيار الأداة الخاطئة لإدارة الحالة. في هذا المقال، لن نتحدث عن النظريات، بل عن ماذا يحدث بالضبط في الذاكرة والمعالج عندما تستخدم useState مقابل Zustand مقابل Redux، ومتى يجب أن تتجنب كل منها.
إدارة الحالة في React ليست مجرد اختيار بين useState وuseReducer. إنها قرار هندسي يؤثر على أداء التطبيق، وصيانة الكود، وحتى على صحة فريق التطوير. الحقيقة هي أن معظم المطورين يختارون الأداة بناءً على ما سمعوه في آخر مؤتمر، وليس بناءً على تحليل حقيقي لاحتياجات المشروع. في هذا الدليل، سنفكك كل أداة على حدة، ونشرح ماذا يحدث خلف الكواليس، ومتى يجب أن تستخدمها — ومتى يجب أن تهرب منها كما تهرب من الكود الذي يستخدم document.getElementById داخل مكون React.
useState هو أول أداة يتعلمها المطورون لإدارة الحالة في React، وغالباً ما يكون آخر أداة يستخدمونها بحكمة. المشكلة ليست في useState نفسه، بل في سوء استخدامه. عندما تكتب const [count, setCount] = useState(0)، فإن React لا يخزن القيمة فقط، بل ينشئ أيضاً آلية لإعادة رسم المكون عند تغيير الحالة. هذا يبدو بسيطاً، لكن ماذا يحدث عندما يكون لديك ٥٠ حالة داخل مكون واحد؟
خلف الكواليس، كل استدعاء لـ useState ينشئ عقدة في شجرة React الداخلية. هذه العقدة ليست مجرد متغير في الذاكرة، بل هي جزء من نظام تتبع التبعيات الذي يستخدمه React لتحديد متى يجب إعادة رسم المكون. عندما تغير الحالة، React لا يعيد رسم المكون فقط، بل يعيد حساب جميع القيم المشتقة داخل المكون، حتى لو لم تتغير. هذا يعني أن استخدام useState داخل مكون كبير ومعقد يشبه وضع محرك صاروخي داخل دراجة هوائية — قد يعمل، لكنك تهدر موارد هائلة دون داعٍ.
// مثال سيء: useState في مكون ضخم
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [comments, setComments] = useState([]);
const [likes, setLikes] = useState(0);
const [isFollowing, setIsFollowing] = useState(false);
const [followers, setFollowers] = useState([]);
const [following, setFollowing] = useState([]);
const [notifications, setNotifications] = useState([]);
const [darkMode, setDarkMode] = useState(false);
// ... 30 سطر آخر من الكود
useEffect(() => {
fetchUserData(userId).then(data => {
setUser(data.user);
setPosts(data.posts);
setComments(data.comments);
// ... تحديث 10 حالات أخرى
});
}, [userId]);
return <div>{/* JSX معقد */}</div>;
}
// المشكلة: تغيير أي حالة يؤدي إلى إعادة رسم المكون بالكامل
// الحل: تقسيم المكون إلى مكونات أصغر، أو استخدام أداة إدارة حالة أفضلالمشكلة الحقيقية هنا ليست في عدد حالات useState، بل في أن كل تغيير في أي حالة يؤدي إلى إعادة رسم المكون بالكامل. إذا كان لديك مكون يحتوي على ٥٠ حالة، وتغير حالة واحدة فقط، React سيضطر لإعادة حساب جميع القيم المشتقة داخل المكون، حتى لو كانت تعتمد على حالات لم تتغير. هذا يشبه تغيير لون سيارة ثم اضطرار المصنع لإعادة بناء المحرك بالكامل لأنها مرتبطة بنفس التصميم.
لكن حتى في هذه الحالات، يجب أن تكون حذراً. إذا وجدت نفسك تمرر الحالة عبر عدة مستويات من المكونات باستخدام props، فهذا مؤشر واضح على أنك بحاجة إلى أداة إدارة حالة أفضل. تمرير الحالة عبر ٥ مستويات من المكونات ليس فقط مزعجاً، بل هو أيضاً علامة على تصميم سيء للمكونات.
عندما يكتشف المطورون Context API لأول مرة، يشعرون وكأنهم وجدوا الحل السحري لجميع مشاكل إدارة الحالة.
فقط ضع الحالة في Context وسأصل إليها من أي مكان!
— مطور مبتدئ في أول يوم له مع React
لكن Context ليس سحرياً، بل هو مجرد طريقة لتمرير البيانات عبر شجرة المكونات دون الحاجة لتمرير props يدوياً. المشكلة هي أن الكثير من المطورين يستخدمونه كبديل لـ Redux أو Zustand، وهذا خطأ فادح. عندما تضع حالة في Context، فإن كل مكون يستهلك هذا Context سيُعاد رسمه عند تغيير الحالة، حتى لو كان التغيير لا يؤثر عليه.
// مثال على Context سيء
const UserC React.createContext();
function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
return (
<UserContext.Provider value={{ user, setUser, theme, setTheme, notifications, setNotifications }}>
<Header />
<MainContent />
<Footer />
</UserContext.Provider>
);
}
function Header() {
const { theme } = useContext(UserContext);
// هذا المكون سيُعاد رسمه عند تغيير user أو notifications
// حتى لو كان لا يستخدمهما!
return <header className={theme}>...</header>;
}في هذا المثال، Header يستخدم فقط theme من Context، لكنه سيُعاد رسمه عند تغيير user أو notifications. هذا لأن React لا يعرف أي جزء من Context يستخدمه المكون، لذلك يعيد رسمه عند تغيير أي قيمة في Context. في تطبيقات كبيرة، هذا يمكن أن يؤدي إلى إعادة رسم مئات المكونات عند تغيير حالة بسيطة، مما يسبب بطء ملحوظ في الأداء.
الحل هو تقسيم Context إلى عدة Contexts صغيرة، كل منها مسؤول عن جزء واحد من الحالة. بدلاً من وضع جميع الحالات في Context واحد، قم بإنشاء Context لكل حالة مستقلة:
// الحل: تقسيم Context
const UserC React.createContext();
const ThemeContext = React.createContext();
const NotificationsContext = React.createContext();
function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
<NotificationsContext.Provider value={{ notifications, setNotifications }}>
<Header />
<MainContent />
<Footer />
</NotificationsContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}
function Header() {
const { theme } = useContext(ThemeContext);
// الآن هذا المكون لن يُعاد رسمه عند تغيير user أو notifications
return <header className={theme}>...</header>;
}بهذه الطريقة، Header لن يُعاد رسمه إلا عند تغيير theme. هذا يحسن الأداء بشكل كبير، لكنه لا يحل مشكلة أخرى: Context لا يدعم التحديثات الجزئية. إذا كان لديك كائن كبير في Context وتغير جزء صغير منه، React سيضطر لإعادة رسم جميع المكونات التي تستهلك هذا Context، حتى لو كانت لا تستخدم الجزء الذي تغير.
إذا كانت حالتك تحتاج لمشاركة عبر التطبيق بأكمله وتتغير بشكل متكرر، فربما Context ليس الخيار الأفضل. في هذه الحالة، يجب أن تفكر في مكتبة إدارة حالة متخصصة مثل Zustand أو Redux.
Zustand هو مكتبة إدارة حالة خفيفة الوزن أصبحت المفضلة لدى الكثير من المطورين في السنوات الأخيرة. السبب؟ إنها تجمع بين بساطة useState وقوة Redux، دون التعقيد الذي يأتي مع Redux. عندما تستخدم Zustand، فأنت تنشئ متجراً واحداً أو أكثر يخزن حالتك، ويمكن لأي مكون الوصول إلى هذا المتجر وتحديثه دون الحاجة لتمرير props أو Context.
خلف الكواليس، Zustand يستخدم نظاماً ذكياً لتتبع التبعيات. بدلاً من إعادة رسم جميع المكونات عند تغيير الحالة، Zustand يسمح لك بتحديد بالضبط أي جزء من الحالة يحتاج المكون إلى مراقبته. هذا يعني أن المكون لن يُعاد رسمه إلا عند تغيير الجزء الذي يعتمد عليه، وليس عند تغيير أي جزء من الحالة. هذا يختلف تماماً عن Context، حيث يُعاد رسم جميع المكونات عند تغيير أي جزء من الحالة.
// مثال على Zustand
import { create } from 'zustand';
const useStore = create((set) => ({
user: null,
theme: 'light',
notifications: [],
setUser: (user) => set({ user }),
setTheme: (theme) => set({ theme }),
addNotification: (notification) => set((state) => ({
notifications: [...state.notifications, notification]
})),
}));
function Header() {
const theme = useStore((state) => state.theme);
// هذا المكون لن يُعاد رسمه عند تغيير user أو notifications
return <header className={theme}>...</header>;
}
function UserProfile() {
const user = useStore((state) => state.user);
// هذا المكون لن يُعاد رسمه عند تغيير theme أو notifications
return <div>{user?.name}</div>;
}في هذا المثال، Header يستخدم فقط theme من المتجر، لذلك لن يُعاد رسمه عند تغيير user أو notifications. هذا لأن Zustand يسمح لك بتحديد الجزء الذي تريد مراقبته باستخدام دالة انتقائية. هذه الميزة وحدها تجعل Zustand أكثر كفاءة من Context في معظم الحالات.
Zustand هو خيار رائع لمعظم التطبيقات، لكنه ليس مثالياً لكل شيء. إذا كنت تعمل على تطبيق كبير جداً وتحتاج إلى ميزات متقدمة مثل Middleware أو DevTools مخصصة، فقد يكون Redux خياراً أفضل. أيضاً، إذا كنت بحاجة إلى إدارة حالة معقدة جداً مع الكثير من العلاقات بين البيانات، فقد تحتاج إلى مكتبة أكثر قوة مثل Jotai أو Recoil.
Redux هو مكتبة إدارة حالة قديمة وموثوقة، لكنها تأتي بثمن: التعقيد. عندما تستخدم Redux، فأنت تضيف الكثير من الكود الإضافي إلى مشروعك: Actions، Reducers، Store، Middleware، وDevTools. هذا يمكن أن يكون مفيداً في التطبيقات الكبيرة جداً، لكنه مبالغ فيه في معظم الحالات.
المشكلة الرئيسية مع Redux ليست في المكتبة نفسها، بل في الطريقة التي يستخدمها بها المطورون. الكثير من المطورين يضيفون Redux إلى مشاريعهم الصغيرة والمتوسطة دون الحاجة إليه، مما يضيف تعقيداً لا داعي له. أيضاً، Redux يشجع على نمط معين من كتابة الكود: فصل الحالة إلى Actions وReducers، وهذا يمكن أن يكون مفيداً في بعض الحالات، لكنه يجعل الكود أكثر صعوبة في الفهم والصيانة في حالات أخرى.
// مثال على Redux (الكود الكامل)
// actions.js
const setUser = (user) => ({
type: 'SET_USER',
payload: user
});
const setTheme = (theme) => ({
type: 'SET_THEME',
payload: theme
});
// reducers.js
const initialState = {
user: null,
theme: 'light'
};
function rootReducer(state = initialState, action) {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'SET_THEME':
return { ...state, theme: action.payload };
default:
return state;
}
}
// store.js
import { createStore } from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer);
// Component.js
import { useSelector, useDispatch } from 'react-redux';
function Header() {
const theme = useSelector((state) => state.theme);
const dispatch = useDispatch();
return (
<header className={theme}>
<button {() => dispatch(setTheme('dark'))}>Toggle Theme</button>
</header>
);
}هذا المثال يوضح مدى تعقيد Redux مقارنة بـ Zustand أو حتى Context. تحتاج إلى كتابة Actions، Reducers، وتكوين Store، وهذا كله من أجل إدارة حالة بسيطة. لكن هذا التعقيد يأتي بفوائد: Redux يوفر نظاماً قوياً لإدارة الحالة مع ميزات مثل Middleware وDevTools المتقدمة. إذا كنت تعمل على تطبيق كبير جداً مع فريق كبير، فقد يكون هذا التعقيد مبرراً.
لكن حتى في هذه الحالات، يجب أن تفكر جيداً قبل إضافة Redux. في عام ٢٠٢٤، هناك بدائل أخف وزناً وأكثر سهولة في الاستخدام مثل Zustand أو Jotai يمكنها التعامل مع معظم حالات الاستخدام دون التعقيد الذي يأتي مع Redux. أيضاً، مع ظهور ميزات جديدة في React مثل Server Components، قد لا تحتاج إلى Redux بنفس القدر الذي كنت تحتاج إليه في الماضي.
في السنوات الأخيرة، ظهرت فكرة جديدة لإدارة الحالة تسمى الإشارات (Signals). الإشارات هي مفهوم مأخوذ من مكتبات مثل SolidJS وPreact، وهي طريقة لإدارة الحالة تعتمد على تتبع التبعيات بشكل دقيق جداً. الفكرة هي أن الإشارات تسمح لك بتحديث الحالة دون الحاجة لإعادة رسم المكونات بأكملها، بل فقط الأجزاء التي تعتمد على الحالة المتغيرة.
في React، بدأت تظهر مكتبات مثل React Signals وJotai التي تستخدم مفهوم الإشارات. الفكرة هي أن الإشارات توفر أداءً أفضل من useState أو Context لأنها تتجنب إعادة الرسم غير الضرورية. لكن هل تحتاج حقاً إلى الإشارات؟ في معظم الحالات، لا. الإشارات تضيف تعقيداً جديداً إلى الكود، ومعظم التطبيقات لا تحتاج إلى هذا المستوى من الأداء.
// مثال على استخدام الإشارات مع Jotai
import { atom, useAtom } from 'jotai';
const countAtom = atom(0);
const doubleAtom = atom((get) => get(countAtom) * 2);
function Counter() {
const [count, setCount] = useAtom(countAtom);
const [double] = useAtom(doubleAtom); // لن يُعاد رسم المكون عند تغيير double
return (
<div>
<button {() => setCount(count + 1)}>Increment</button>
<div>Count: {count}</div>
<div>Double: {double}</div>
</div>
);
}في هذا المثال، doubleAtom هو ذرة مشتقة تعتمد على countAtom. عندما يتغير countAtom، doubleAtom يتغير تلقائياً، لكن المكون لن يُعاد رسمه إلا عند تغيير countAtom. هذا لأن Jotai يستخدم نظام تتبع تبعيات دقيق جداً. هذا يبدو رائعاً، لكن هل تحتاج حقاً إلى هذا التعقيد؟ في معظم الحالات، Zustand أو حتى Context يمكن أن يكونا كافيين.
لكن كن حذراً: الإشارات ليست حلاً سحرياً. في معظم التطبيقات، الفرق في الأداء بين الإشارات وZustand سيكون ضئيلاً جداً، بينما الفرق في التعقيد سيكون كبيراً. قبل أن تقرر استخدام الإشارات، اسأل نفسك: هل حقاً أحتاج إلى هذا المستوى من الأداء؟ أم أنني فقط أريد تجربة شيء جديد؟
بعد كل هذا الشرح، كيف تختار الأداة المناسبة لإدارة الحالة في مشروعك؟ إليك خريطة بسيطة تساعدك على اتخاذ القرار:
لكن تذكر: هذه الخريطة ليست قواعد صارمة. في بعض الحالات، قد تحتاج إلى استخدام أكثر من أداة في نفس المشروع. على سبيل المثال، يمكنك استخدام Zustand لإدارة الحالة العامة، وuseState للحالة المحلية، وContext للإعدادات التي لا تتغير كثيراً. المفتاح هو فهم احتياجات مشروعك وتجربة الأدوات المختلفة حتى تجد ما يناسبك.
إذا كنت تريد نصيحة واحدة فقط من هذا المقال، فهي هذه: لا تختر الأداة بناءً على الشعبية أو الضجة، بل اخترها بناءً على تحليل حقيقي لاحتياجات مشروعك. قبل أن تضيف أي مكتبة لإدارة الحالة، اسأل نفسك هذه الأسئلة:
في معظم الحالات، Zustand هو الخيار الأفضل للمشاريع المتوسطة والكبيرة، بينما useState وContext كافيان للمشاريع الصغيرة. Redux مازال له مكانه في التطبيقات الكبيرة جداً، لكن الإشارات ما زالت تقنية ناشئة قد لا تحتاج إليها. وفي النهاية، أفضل أداة لإدارة الحالة هي تلك التي تفهمها جيداً وتستطيع صيانتها بسهولة.