هل تستخدم useState لكل شيء؟ أم أن Redux أصبح عبئاً على مشروعك؟ اكتشف متى تختار Context، Zustand، أو حتى Signals، وكيف تتجنب الكوارث في الذاكرة والأداء.
في أحد المشاريع الكبيرة التي عملت عليها العام الماضي، كان لدينا مكون واحد فقط مسؤول عن إدارة حالة المستخدم، الفواتير، وإعدادات الواجهة. بعد ثلاثة أشهر من التطوير، أصبح الملف يتعدى ١٢٠٠ سطر، وكل مرة نغير فيها سطراً واحداً، كان التطبيق بأكمله يعيد تحميل نفسه خمس مرات متتالية. المشكلة لم تكن في منطق الكود، بل في أننا اخترنا الأداة الخاطئة لإدارة الحالة منذ البداية. هنا تأتي أهمية السؤال: متى تستخدم useState، ومتى تنتقل إلى شيء مثل Zustand أو حتى Redux؟ الإجابة ليست مجرد تفضيل شخصي، بل قرار هندسي يؤثر على أداء التطبيق، قابلية الصيانة، وحتى على صحة فريق التطوير.
الخطأ الشائع الذي أراه في معظم المشاريع هو استخدام أداة واحدة لإدارة كل أنواع الحالة. مثلاً، بعض الفرق تستخدم Redux حتى لإدارة حالة زر بسيط مثل 'هل القائمة مفتوحة أم لا'، بينما أخرى تعتمد على useState حتى في تطبيقات معقدة تحتوي على مئات المكونات. الحقيقة هي أن كل أداة لها سيناريو محدد تعمل فيه بكفاءة، وخارج هذا السيناريو تصبح عبئاً. في هذا الدليل، سنفكك كل أداة على حدة، ونشرح ماذا يحدث خلف الكواليس في الذاكرة والمعالج، ونحدد بالضبط متى تستخدم ماذا دون تعقيد.
لنبدأ بالأداة التي يعرفها الجميع: useState. هذه الـ Hook بسيطة جداً في مظهرها، لكنها قوية جداً عندما تستخدم في المكان الصحيح. عندما تكتب const [count, setCount] = useState(0)، فإن React لا يخزن القيمة فقط، بل ينشئ أيضاً آلية كاملة لإعادة رسم المكون عند تغيير الحالة. خلف الكواليس، React يحتفظ بقيمة الحالة في ذاكرة خاصة تسمى 'Fiber Tree'، وعندما تستدعي setCount، فإن React لا يعدل القيمة فوراً، بل يضيف تحديثاً إلى قائمة التحديثات التي ستطبق في الدورة القادمة من الـ Event Loop.
الميزة الرئيسية لـ useState هي أنها محلية تماماً للمكون. هذا يعني أن تغيير الحالة في مكون واحد لن يؤثر على المكونات الأخرى، ما لم تمرر الحالة كprops. هذه العزلة تجعلها مثالية للحالات البسيطة مثل: هل القائمة مفتوحة؟ ما هو النص المدخل في حقل البحث؟ هل الزر مضغوط؟ لكن هذه العزلة نفسها تصبح مشكلة عندما تحتاج إلى مشاركة الحالة بين مكونات متعددة، خاصة إذا كانت هذه المكونات متباعدة في شجرة المكونات. هنا تبدأ المشاكل، لأنك ستضطر إلى تمرير الحالة عبر عدة مستويات من المكونات، وهو ما يعرف بـ 'Prop Drilling'، وهذا يؤدي إلى كود صعب القراءة والصيانة.
// مثال على Prop Drilling السيئ
function ParentComponent() {
const [user, setUser] = useState(null);
return <ChildComponent user={user} setUser={setUser} />;
}
function ChildComponent({ user, setUser }) {
return <GrandChildComponent user={user} setUser={setUser} />;
}
function GrandChildComponent({ user, setUser }) {
// هنا نستخدم user و setUser بعد تمريرهما عبر مستويين
return <div>{user?.name}</div>;
}من تجربتي، useState هي الخيار الأمثل عندما تكون الحالة محلية ومحدودة النطاق. مثلاً، في مكون تسجيل الدخول، يمكنك استخدام useState لإدارة حالة الحقول المدخلة، وحالة التحميل، وحالة الخطأ. لكن بمجرد أن تحتاج إلى مشاركة هذه الحالة مع مكون آخر، أو حتى مع نفس المكون بعد إعادة تحميله، يصبح من الأفضل التفكير في أداة أخرى. أيضاً، useState ليست مناسبة للحالات التي تحتاج إلى منطق معقد عند التحديث، مثل التحديثات المشروطة أو التحديثات التي تعتمد على القيمة السابقة. في هذه الحالات، قد تحتاج إلى استخدام useReducer بدلاً منها.
عندما تحتاج إلى مشاركة الحالة بين مكونات متعددة دون المرور بـ Prop Drilling، فإن Context API هو الحل الطبيعي. الفكرة الأساسية وراء Context هي إنشاء مخزن مركزي يمكن لأي مكون الوصول إليه دون الحاجة إلى تمرير الحالة عبر كل المستويات. لكن هناك فكرة خاطئة شائعة حول Context، وهي أنه بديل لـ Redux أو Zustand. الحقيقة هي أن Context ليس أداة لإدارة الحالة بحد ذاته، بل هو أداة لمشاركة الحالة. بمعنى آخر، Context لا يوفر لك آلية لتحديث الحالة، بل يوفر لك طريقة للوصول إلى الحالة التي تديرها باستخدام useState أو useReducer.
المشكلة الرئيسية في Context هي أنه يؤدي إلى إعادة رسم جميع المكونات التي تستهلكه عند تغيير الحالة، حتى لو كان التغيير لا يؤثر على هذه المكونات. هذا يحدث لأن React لا يملك طريقة لمعرفة أي جزء من الحالة تغير، لذلك يعيد رسم كل شيء. مثلاً، إذا كان لديك Context يحتوي على user و theme، وتغيرت قيمة user، فإن جميع المكونات التي تستهلك هذا Context ستعيد الرسم، حتى تلك التي تستخدم theme فقط. هذا يمكن أن يؤدي إلى مشاكل أداء كبيرة في التطبيقات الكبيرة، خاصة إذا كان Context يحتوي على حالة متغيرة بشكل متكرر.
// مثال على Context غير الأمثل
const AppC createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
return (
<AppContext.Provider value={{ user, setUser, theme, setTheme }}>
{children}
</AppContext.Provider>
);
}
// في مكون بعيد
function UserProfile() {
const { user, theme } = useContext(AppContext);
// هذا المكون سيعيد الرسم عند تغيير user أو theme
return <div style={{ color: theme === 'dark' ? 'white' : 'black' }}>{user?.name}</div>;
}لحل هذه المشكلة، يمكنك تقسيم Context إلى عدة Contexts أصغر، بحيث يحتوي كل Context على جزء واحد من الحالة. مثلاً، يمكنك إنشاء UserContext و ThemeContext بدلاً من AppContext واحد. بهذه الطريقة، إذا تغير user، فإن المكونات التي تستهلك ThemeContext فقط لن تعيد الرسم. هذا الحل فعال، لكنه قد يؤدي إلى تعقيد الكود إذا كان لديك العديد من Contexts الصغيرة. أيضاً، Context ليس مناسباً للحالات التي تحتاج إلى تحديثات متكررة جداً، مثل عدادات الوقت الحقيقي أو تطبيقات الدردشة، لأن كل تحديث سيؤدي إلى إعادة رسم جميع المكونات المستهلكة.
إذا كنت تبحث عن أداة تجمع بين بساطة useState وقوة مشاركة الحالة مثل Redux، فإن Zustand هو الخيار الأمثل. Zustand هو مكتبة صغيرة لإدارة الحالة تعتمد على مفهوم الـ 'Store'، حيث يمكنك إنشاء مخزن مركزي يحتوي على الحالة والمنطق الخاص بتحديثها. الفرق الرئيسي بين Zustand و Redux هو أن Zustand لا يتطلب منك كتابة الكثير من الكود الإلزامي مثل Actions و Reducers، بل يمكنك ببساطة تعريف الحالة والمنطق في مكان واحد، ثم الوصول إليها من أي مكون باستخدام hook مخصص.
أحد أكبر مزايا Zustand هو أنه يحل مشكلة إعادة الرسم غير الضرورية التي تحدث مع Context. Zustand يستخدم تقنية تسمى 'Selector' تسمح لك باختيار جزء محدد من الحالة، بحيث يعيد المكون الرسم فقط عندما يتغير هذا الجزء المحدد. مثلاً، إذا كان لديك مخزن يحتوي على user و theme، يمكنك إنشاء selector لكل منهما، بحيث يعيد المكون الرسم فقط عندما يتغير الجزء الذي يهمه. هذا يجعل Zustand أكثر كفاءة من Context في التطبيقات الكبيرة التي تحتوي على حالة متغيرة بشكل متكرر.
// مثال على Zustand Store
import create from 'zustand';
const useStore = create((set) => ({
user: null,
theme: 'light',
setUser: (user) => set({ user }),
toggleTheme: () => set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })),
}));
// في مكون بعيد
function UserProfile() {
const user = useStore((state) => state.user);
const theme = useStore((state) => state.theme);
// هذا المكون سيعيد الرسم فقط عند تغيير user أو theme
return <div style={{ color: theme === 'dark' ? 'white' : 'black' }}>{user?.name}</div>;
}من تجربتي، Zustand هو الخيار الأفضل لمعظم التطبيقات المتوسطة إلى الكبيرة التي تحتاج إلى إدارة حالة معقدة دون التعقيد الذي يأتي مع Redux. Zustand أيضاً يدعم ميزات متقدمة مثل Middleware، مما يسمح لك بإضافة منطق إضافي عند تحديث الحالة، مثل تسجيل التغييرات أو التفاعل مع واجهة برمجة التطبيقات. أيضاً، Zustand يعمل بشكل جيد مع Server-Side Rendering و Static Site Generation، مما يجعله مناسباً لمشاريع Next.js.
Redux هو أحد أقدم أدوات إدارة الحالة في React، ولا يزال يستخدم على نطاق واسع في التطبيقات الكبيرة والمعقدة. الفكرة الأساسية وراء Redux هي فصل الحالة عن منطق التحديث، حيث يتم تعريف الحالة في مكان واحد يسمى 'Store'، ويتم تحديثها باستخدام 'Actions' و 'Reducers'. هذا الفصل يجعل الكود أكثر قابلية للتنبؤ والصيانة، خاصة في التطبيقات الكبيرة التي تحتوي على حالة معقدة ومنطق تحديث متشابك.
لكن Redux يأتي مع تعقيداته الخاصة. أولاً، يتطلب منك كتابة الكثير من الكود الإلزامي حتى لأبسط التحديثات. مثلاً، لتغيير قيمة واحدة في الحالة، تحتاج إلى تعريف Action، و Reducer، وربما حتى Action Creator. هذا يمكن أن يؤدي إلى الكثير من الكود المتكرر، خاصة في التطبيقات الصغيرة أو المتوسطة. ثانياً، Redux ليس الأمثل للأداء في التطبيقات التي تحتوي على حالة متغيرة بشكل متكرر، لأن كل تحديث يؤدي إلى إعادة رسم جميع المكونات المتصلة بالـ Store، إلا إذا استخدمت مكتبات إضافية مثل Reselect لتقليل إعادة الرسم.
// مثال على Redux مع الكثير من الكود الإلزامي
// Actions
const SET_USER = 'SET_USER';
const TOGGLE_THEME = 'TOGGLE_THEME';
const setUser = (user) => ({ type: SET_USER, payload: user });
const toggleTheme = () => ({ type: TOGGLE_THEME });
// Reducer
const initialState = {
user: null,
theme: 'light',
};
function appReducer(state = initialState, action) {
switch (action.type) {
case SET_USER:
return { ...state, user: action.payload };
case TOGGLE_THEME:
return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' };
default:
return state;
}
}
// Store
const store = createStore(appReducer);
// في مكون
function UserProfile() {
const user = useSelector((state) => state.user);
const theme = useSelector((state) => state.theme);
const dispatch = useDispatch();
return (
<div style={{ color: theme === 'dark' ? 'white' : 'black' }}>
{user?.name}
<button {() => dispatch(toggleTheme())}>Toggle Theme</button>
</div>
);
}من تجربتي، Redux هو الخيار الأفضل عندما تعمل على تطبيق كبير ومعقد يحتوي على حالة متشابكة ومنطق تحديث معقد. مثلاً، في تطبيقات التجارة الإلكترونية الكبيرة، حيث تحتاج إلى إدارة حالة المستخدم، السلة، المنتجات، الفواتير، وغيرها، يمكن لـ Redux توفير الهيكلية اللازمة لجعل الكود قابلاً للصيانة. لكن في التطبيقات الصغيرة أو المتوسطة، يمكن أن يكون Redux عبئاً غير ضروري، خاصة إذا كنت لا تستفيد من ميزاته المتقدمة مثل Middleware و DevTools.
مع صدور React 18 وإدخال ميزات مثل Concurrent Mode و Server Components، أصبح السؤال أكثر تعقيداً: هل ما زلنا بحاجة إلى أدوات إدارة الحالة التقليدية؟ الإجابة القصيرة هي نعم، ولكن مع بعض التحفظات. Concurrent Mode يسمح لـ React بتقسيم العمل إلى أجزاء صغيرة يمكن معالجتها بشكل متزامن، مما يحسن أداء التطبيقات الكبيرة. لكن هذا لا يلغي الحاجة إلى إدارة الحالة، بل يجعل بعض الأدوات التقليدية أقل كفاءة في بعض السيناريوهات.
أحد التحديات الجديدة التي جاءت مع React 18 هو إدارة الحالة في Server Components. في Server Components، لا يمكنك استخدام Hooks مثل useState أو useEffect، لأن هذه المكونات يتم تنفيذها على السيرفر وليس في المتصفح. هذا يعني أنك بحاجة إلى أدوات جديدة لإدارة الحالة في هذه المكونات، مثل استخدام Context على مستوى السيرفر أو الاعتماد على مكتبات مثل Zustand التي تدعم SSR. أيضاً، مع إدخال Server Actions في Next.js، أصبح من الممكن تحديث الحالة على السيرفر دون الحاجة إلى كتابة الكثير من الكود الإلزامي، مما يقلل من الحاجة إلى أدوات مثل Redux في بعض السيناريوهات.
// مثال على Server Component مع Zustand
// app/store.js
import { create } from 'zustand';
export const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
// app/page.js (Server Component)
export default function Page() {
// لا يمكن استخدام useStore هنا لأن هذا مكون سيرفر
return <ClientComponent />;
}
// app/client-component.js (Client Component)
'use client';
import { useStore } from './store';
export default function ClientComponent() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
return (
<div>
<p>Count: {count}</p>
<button {increment}>Increment</button>
</div>
);
}من تجربتي، React 18 لم يلغِ الحاجة إلى أدوات إدارة الحالة، بل غير السيناريوهات التي نستخدم فيها كل أداة. مثلاً، في التطبيقات التي تستخدم Server Components بشكل مكثف، قد تجد نفسك تعتمد أكثر على Zustand أو حتى على حلول مخصصة لإدارة الحالة على السيرفر. أيضاً، مع إدخال ميزات مثل useTransition و useDeferredValue، أصبح من الممكن تحسين أداء التطبيقات التي تعتمد على حالة متغيرة بشكل متكرر دون الحاجة إلى أدوات خارجية. لكن في النهاية، القرار يعتمد على طبيعة التطبيق ومتطلبات الأداء وقابلية الصيانة.
بعد أكثر من عشر سنوات في تطوير تطبيقات React، توصلت إلى قاعدة بسيطة: لا تختر الأداة بناءً على شعبيتها أو تعقيدها، بل اخترها بناءً على طبيعة الحالة التي تديرها. إذا كانت الحالة محلية ومحدودة النطاق، استخدم useState. إذا كانت تحتاج إلى مشاركة بين مكونات متعددة ولكن لا تتغير بشكل متكرر، استخدم Context. إذا كانت الحالة معقدة وتحتاج إلى منطق تحديث متقدم، استخدم Zustand أو Redux بناءً على حجم التطبيق وتعقيده. وإذا كنت تعمل على تطبيق يستخدم Server Components بشكل مكثف، فكر في استخدام Zustand أو حتى حلول مخصصة لإدارة الحالة على السيرفر.
القرار النهائي يعتمد على عدة عوامل: حجم التطبيق، تعقيد الحالة، فريق التطوير، ومتطلبات الأداء. لكن تذكر دائماً أن الهدف ليس استخدام أحدث الأدوات أو الأكثر تعقيداً، بل بناء تطبيق سريع، قابل للصيانة، وسهل الفهم. أحياناً، أبسط الأدوات هي الأفضل، حتى لو لم تكن الأكثر شهرة. وفي النهاية، إدارة الحالة ليست مجرد مسألة تقنية، بل هي قرار هندسي يؤثر على كل جزء من التطبيق.
إدارة الحالة في React ليست مجرد اختيار أداة، بل هي فن موازنة بين البساطة والأداء والقابلية للصيانة. الأداة الخاطئة يمكن أن تدمر تطبيقك، والأداة الصحيحة يمكن أن تجعله يطير.
— تجربة شخصية بعد عشر سنوات في تطوير React