هل تستخدم useState لكل شيء؟ أم أنك غرقت في Redux دون داع؟ اكتشف متى تختار Context، Redux، Zustand، أو حتى Signals، وكيف يتصرف كل منها في الذاكرة والمعالج تحت الضغط.
في عام ٢٠٢٣، كان هناك مشروع ضخم يستخدم Redux لإدارة حالة زر بسيط يفتح قائمة منسدلة. النتيجة؟ ٨٠ كيلوبايت إضافية من الجافاسكريبت، و٣٠٠ مللي ثانية تأخير في الـ render الأولي، ومطورون يقضون ساعات في كتابة actions و reducers لحالة لا تتغير إلا مرة واحدة في اليوم. المشكلة ليست في Redux نفسه، بل في أننا نستخدم الأداة الخطأ للمهمة الخطأ. إدارة الحالة في React ليست مجرد اختيار بين useState و Redux، بل هي قرار هندسي يؤثر على أداء التطبيق، وصيانة الكود، وحتى صحة فريق التطوير.
الحقيقة هي أن معظم المطورين لا يفهمون تماماً ماذا يحدث خلف الكواليس عندما يستخدمون Context API أو Zustand. هل تعلم مثلاً أن كل تحديث في Context يؤدي إلى إعادة render لكل مكون يستخدم هذا الـ Context، حتى لو كان التحديث لا يؤثر على جزء معين من الـ state؟ أو أن Redux يستخدم تقنية تسمى memoization لمنع التحديثات غير الضرورية، لكنها تأتي بتكلفة إضافية في الذاكرة؟ في هذا الدليل، سنفكك كل أداة إدارة حالة شائعة في React، ونشرح متى تستخدمها، وماذا يحدث بالضبط في الـ Event Loop والذاكرة عندما تفعل ذلك، وكيف تتجنب الفخاخ التي يقع فيها حتى المطورون المحترفون.
useState هو أول أداة إدارة حالة يتعلمها أي مطور React، وغالباً ما يُنظر إليه على أنه حل بسيط للمشاكل البسيطة. لكن الحقيقة هي أن useState هو أقوى أداة لديك عندما يتعلق الأمر بالحالة المحلية التي لا تحتاج إلى مشاركة بين مكونات متعددة. لماذا؟ لأنه خفيف الوزن للغاية، ولا يضيف أي تعقيد إلى الكود، ويعمل بشكل مباشر مع React's reconciliation algorithm لتحديث الـ DOM بكفاءة.
عندما تستدعي useState، React يخصص مساحة في الذاكرة لتخزين القيمة الأولية، ويعيد لك مصفوفة تحتوي على القيمة الحالية و function لتحديثها. التحديث هنا يحدث بشكل غير متزامن في الـ Event Loop، مما يعني أن React قد يجمع عدة تحديثات لـ state في دفعة واحدة لتحسين الأداء. لكن هناك فخ كبير هنا: إذا قمت بتحديث الـ state بناءً على قيمته الحالية، يجب عليك استخدام الـ functional update لتجنب مشاكل التزامن. مثلاً، إذا كتبت setCount(count + 1) داخل loop، قد تفقد بعض التحديثات لأن الـ state السابق قد لا يكون محدثاً بعد.
// ❌ خطأ شائع: قد تفقد تحديثات بسبب التزامن
for (let i = 0; i < 5; i++) {
setCount(count + 1); // count قد لا يكون محدثاً
}
// ✅ الحل الصحيح: استخدام functional update
for (let i = 0; i < 5; i++) {
setCount(prevCount => prevCount + 1); // يضمن استخدام القيمة المحدثة
}
// مثال آخر: تحديث كائن معتمد على حالته السابقة
const [user, setUser] = useState({ name: 'Ahmed', age: 30 });
// ❌ خطأ: قد تفقد خصائص أخرى من الكائن
setUser({ age: user.age + 1 });
// ✅ الحل الصحيح: استخدام spread operator مع functional update
setUser(prevUser => ({ ...prevUser, age: prevUser.age + 1 }));من تجربتي، الكثير من المطورين ينتقلون إلى حلول أكثر تعقيداً مثل Redux أو Context لأنهم يعتقدون أن useState لا يمكنه التعامل مع حالات معقدة. لكن الحقيقة هي أن useState يمكنه التعامل مع أي حالة محلية بكفاءة، طالما أنك تتبع أفضل الممارسات. مثلاً، إذا كان لديك مكون يحتوي على عدة حقول في form، يمكنك استخدام useState مع كائن واحد بدلاً من عدة useState calls، مما يقلل من عدد الـ re-renders ويجعل الكود أكثر نظافة.
Context API هو الحل المدمج في React لمشاركة الحالة بين المكونات دون الحاجة إلى تمرير الـ props يدوياً عبر كل مستوى في شجرة المكونات. الفكرة بسيطة: أنشئ Context، وضع فيه القيمة التي تريد مشاركتها، ثم استخدم Provider لتوفير هذه القيمة لجميع المكونات الأبناء. أي مكون داخل شجرة الـ Provider يمكنه الوصول إلى هذه القيمة باستخدام useContext hook.
لكن هنا تكمن المشكلة: Context API ليس حلاً سحرياً. كل تحديث في القيمة التي يوفرها الـ Provider يؤدي إلى إعادة render لكل مكون يستخدم هذا الـ Context، حتى لو كان التحديث لا يؤثر على جزء معين من الـ state. مثلاً، إذا كان لديك Context يحتوي على كائن به ١٠ خصائص، وتحديث خاصية واحدة فقط، فإن كل مكون يستخدم هذا الـ Context سيُعاد renderه، حتى لو كان يستخدم خاصية واحدة فقط لا علاقة لها بالتحديث. هذا السلوك يمكن أن يؤدي إلى مشاكل أداء كبيرة في التطبيقات الكبيرة.
// مثال على Context يؤدي إلى إعادة render غير ضرورية
const UserC React.createContext();
function App() {
const [user, setUser] = useState({
name: 'Ahmed',
age: 30,
email: 'ahmed@example.com',
theme: 'dark',
language: 'ar'
});
return (
<UserContext.Provider value={user}>
<UserProfile />
<UserSettings />
</UserContext.Provider>
);
}
function UserProfile() {
const user = useContext(UserContext);
// هذا المكون سيُعاد renderه عند تحديث أي خاصية في user
// حتى لو كان التحديث متعلق بـ theme أو language فقط
return <div>{user.name}</div>;
}
function UserSettings() {
const user = useContext(UserContext);
// هذا المكون أيضاً سيُعاد renderه عند تحديث أي خاصية
return <div>{user.theme}</div>;
}الحل لهذه المشكلة هو تقسيم الـ Context إلى عدة Contexts أصغر، كل منها مسؤول عن جزء محدد من الـ state. مثلاً، يمكنك إنشاء UserContext للمعلومات الشخصية، و ThemeContext للإعدادات المتعلقة بالمظهر، و LanguageContext للغة التطبيق. بهذه الطريقة، تحديث الـ theme مثلاً لن يؤدي إلى إعادة render للمكونات التي تستخدم UserContext فقط.
// الحل: تقسيم Context إلى عدة Contexts أصغر
const UserC React.createContext();
const ThemeContext = React.createContext();
const LanguageContext = React.createContext();
function App() {
const [user, setUser] = useState({ name: 'Ahmed', age: 30 });
const [theme, setTheme] = useState('dark');
const [language, setLanguage] = useState('ar');
return (
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<LanguageContext.Provider value={language}>
<UserProfile />
<UserSettings />
</LanguageContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}
function UserProfile() {
const user = useContext(UserContext);
// هذا المكون لن يُعاد renderه عند تحديث theme أو language
return <div>{user.name}</div>;
}
function UserSettings() {
const theme = useContext(ThemeContext);
const language = useContext(LanguageContext);
// هذا المكون لن يُعاد renderه عند تحديث user
return <div>{theme} - {language}</div>;
}Redux هو مكتبة إدارة حالة شائعة جداً في مجتمع React، وغالباً ما يُنظر إليه على أنه الحل النهائي لإدارة الحالة في التطبيقات الكبيرة. الفكرة الأساسية في Redux هي أن لديك store واحد يحتوي على كل حالة التطبيق، ويمكنك تحديث هذه الحالة فقط من خلال إرسال actions، والتي تعالجها reducers نقية. هذا النمط يجعل من السهل تتبع التغييرات في الحالة وفهم تدفق البيانات في التطبيق.
لكن Redux يأتي بتكلفة كبيرة: التعقيد. لإعداد Redux، تحتاج إلى كتابة الكثير من الكود التمهيدي (boilerplate)، بما في ذلك actions، reducers، و store. بالإضافة إلى ذلك، Redux ليس جزءاً من React نفسه، مما يعني أنك تحتاج إلى تعلم مفاهيم جديدة مثل middleware و time-travel debugging. من تجربتي، الكثير من المطورين يستخدمون Redux لأنهم سمعوا أنه الحل الأفضل للتطبيقات الكبيرة، لكنهم ينتهي بهم الأمر بكتابة الكثير من الكود غير الضروري والتعامل مع تعقيدات لا يحتاجون إليها حقاً.
// مثال على الكود التمهيدي المطلوب لإعداد Redux
// actions.js
const increment = () => ({ type: 'INCREMENT' });
const decrement = () => ({ type: 'DECREMENT' });
// reducers.js
const initialState = { count: 0 };
function counterReducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
default:
return state;
}
}
// store.js
import { createStore } from 'redux';
import counterReducer from './reducers';
const store = createStore(counterReducer);
// استخدام Store في مكون React
import { Provider, useSelector, useDispatch } from 'react-redux';
function Counter() {
const count = useSelector(state => state.count);
const dispatch = useDispatch();
return (
<div>
<button {() => dispatch(decrement())}>-</button>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
</div>
);
}
function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}هناك أيضاً مشكلة الأداء في Redux. على الرغم من أن Redux يستخدم memoization لمنع التحديثات غير الضرورية، إلا أن هذا يأتي بتكلفة إضافية في الذاكرة. بالإضافة إلى ذلك، إذا كان لديك store كبير يحتوي على الكثير من البيانات، فإن كل تحديث قد يؤدي إلى إعادة حساب الكثير من القيم، مما يؤثر على أداء التطبيق. من تجربتي، الكثير من المطورين يستخدمون Redux دون الحاجة إليه، وينتهي بهم الأمر بإضافة الكثير من التعقيد إلى التطبيق دون فوائد حقيقية.
Zustand هي مكتبة إدارة حالة حديثة اكتسبت شعبية كبيرة في السنوات الأخيرة بسبب بساطتها وكفاءتها. الفكرة الأساسية في Zustand هي أن لديك store واحد أو أكثر، ويمكنك تحديث الحالة مباشرة باستخدام set function، دون الحاجة إلى كتابة actions أو reducers. هذا يجعل Zustand خياراً رائعاً للمطورين الذين يريدون حلاً بسيطاً وفعالاً لإدارة الحالة دون التعقيد الذي يأتي مع Redux.
ما يميز Zustand هو أنه خفيف الوزن للغاية، ولا يتطلب الكثير من الكود التمهيدي. بالإضافة إلى ذلك، Zustand يدعم ميزات متقدمة مثل middleware و devtools، مما يجعله مناسباً للتطبيقات الكبيرة والمعقدة. من تجربتي، Zustand هو الخيار الأفضل عندما تريد حلاً بسيطاً وفعالاً لإدارة الحالة دون التضحية بالمرونة والقوة.
// مثال بسيط على استخدام Zustand
import create from 'zustand';
const useStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 })),
}));
function Counter() {
const { count, increment, decrement } = useStore();
return (
<div>
<button {decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
}
// مثال متقدم: استخدام middleware مع Zustand
const useStoreWithMiddleware = create(
devtools(
persist(
set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 })),
}),
{
name: 'counter-storage', // اسم التخزين المحلي
}
)
)
);Zustand أيضاً يدعم ميزة تسمى 'selectors'، والتي تسمح لك باختيار جزء محدد من الـ state لتجنب إعادة render المكونات غير الضرورية. هذه الميزة تجعل Zustand أكثر كفاءة من Context API في الكثير من الحالات، خاصة عندما يكون لديك حالة معقدة تحتاج إلى مشاركة بين مكونات متعددة.
// استخدام selectors مع Zustand لتجنب إعادة render غير ضرورية
const useStore = create(set => ({
user: { name: 'Ahmed', age: 30 },
theme: 'dark',
language: 'ar',
setUser: user => set({ user }),
setTheme: theme => set({ theme }),
setLanguage: language => set({ language }),
}));
function UserProfile() {
// هذا المكون سيُعاد renderه فقط عند تحديث user
const user = useStore(state => state.user);
return <div>{user.name}</div>;
}
function UserSettings() {
// هذا المكون سيُعاد renderه فقط عند تحديث theme أو language
const theme = useStore(state => state.theme);
const language = useStore(state => state.language);
return <div>{theme} - {language}</div>;
}Signals هي مفهوم جديد نسبياً في عالم إدارة الحالة، وقد بدأت تكتسب شعبية كبيرة بفضل مكتبات مثل Preact Signals و SolidJS. الفكرة الأساسية في Signals هي أن لديك قيم قابلة للتتبع (signals)، وعندما تتغير هذه القيم، فإنها تُعلم المكونات التي تستخدمها فقط، مما يؤدي إلى تحديثات دقيقة وفعالة للغاية. هذا يختلف عن React's reconciliation algorithm، الذي يعيد render المكون بأكمله عند تحديث الـ state.
في React، يمكنك استخدام Signals من خلال مكتبات مثل @preact/signals-react، والتي تسمح لك بإنشاء signals واستخدامها داخل مكونات React. الميزة الكبيرة هنا هي أن Signals توفر أداءً أفضل بكثير من useState و Context API في الكثير من الحالات، خاصة عندما يكون لديك حالة معقدة تحتاج إلى مشاركة بين مكونات متعددة. بالإضافة إلى ذلك، Signals تعمل بشكل جيد مع الـ fine-grained reactivity، مما يعني أن المكونات تُحدث فقط الأجزاء التي تعتمد على الـ signal المتغير.
// مثال على استخدام Preact Signals في React
import { signal, computed } from '@preact/signals-react';
// إنشاء signals
const count = signal(0);
const doubleCount = computed(() => count.value * 2);
function Counter() {
// استخدام signals في مكون React
return (
<div>
<button {() => count.value--}>-</button>
<span>{count}</span>
<span> (Double: {doubleCount})</span>
<button onClick={() => count.value++}>+</button>
</div>
);
}
// مثال متقدم: استخدام signals مع مكونات متعددة
const user = signal({ name: 'Ahmed', age: 30 });
function UserProfile() {
return <div>{user.value.name}</div>;
}
function UserAge() {
return <div>{user.value.age}</div>;
}
function App() {
return (
<div>
<UserProfile />
<UserAge />
<button onClick={() => user.value = { ...user.value, age: user.value.age + 1 }}>
Increase Age
</button>
</div>
);
}ما يميز Signals هو أداؤها العالي جداً. لأن Signals تتبع الاعتمادات بين القيم والمكونات بدقة، فإنها تستطيع تحديث فقط الأجزاء التي تحتاج إلى تحديث، دون إعادة render المكونات بأكملها. هذا يجعل Signals خياراً رائعاً للتطبيقات التي تتطلب أداءً عالياً، مثل الألعاب أو التطبيقات التي تحتوي على الكثير من البيانات المتحركة.
الآن بعد أن استعرضنا كل الأدوات، كيف تختار الأداة المناسبة لمشروعك؟ القرار يعتمد على عدة عوامل، بما في ذلك حجم التطبيق، وتعقيد الحالة، ومتطلبات الأداء، وحجم الفريق. إليك خارطة طريق بسيطة لمساعدتك في اتخاذ القرار:
من تجربتي، الكثير من المطورين يختارون Redux لأنهم سمعوا أنه الحل الأفضل للتطبيقات الكبيرة، لكنهم ينتهي بهم الأمر بإضافة الكثير من التعقيد إلى التطبيق دون فوائد حقيقية. الحقيقة هي أن معظم التطبيقات لا تحتاج إلى Redux، ويمكن إدارتها بكفاءة باستخدام Zustand أو حتى Context API مع تقسيم مناسب. إذا كنت تعمل على تطبيق جديد، ابدأ بأبسط حل ممكن (useState أو Context API)، ثم انتقل إلى حلول أكثر تعقيداً فقط عندما تحتاج إليها حقاً.
هناك أيضاً قاعدة ذهبية يجب أن تتبعها دائماً: لا تختر الأداة بناءً على شعبيتها أو ضجيج المجتمع، بل اخترها بناءً على احتياجات مشروعك الفعلية. مثلاً، إذا كان لديك تطبيق صغير يحتوي على بضعة مكونات فقط، فلا داعي لاستخدام Zustand أو Redux. استخدم useState و Context API، وستكون سعيداً بالبساطة والأداء.
إذا أخذت شيئاً واحداً فقط من هذا المقال، فليكن هذا: ابدأ دائماً بأبسط حل ممكن لإدارة الحالة في React. استخدم useState للحالة المحلية، و Context API لمشاركة الحالة البسيطة بين المكونات، وانتقل إلى Zustand أو Redux فقط عندما تحتاج إلى ميزات متقدمة أو أداء أفضل. ولا تنسَ أن تقيس أداء التطبيق دائماً قبل وبعد إضافة أي مكتبة جديدة، لأن الأداة التي تختارها قد تكون السبب في بطء التطبيق أو تعقيده دون داع.
وأخيراً، تذكر أن إدارة الحالة ليست مجرد اختيار أداة، بل هي قرار هندسي يؤثر على كل جزء من التطبيق. خذ وقتك في فهم احتياجات مشروعك، واختر الأداة التي تناسبها بشكل أفضل، وستوفر على نفسك وعلى فريقك الكثير من الوقت والجهد في المستقبل.