هل useState كافٍ أم تحتاج Redux؟ متى تنتقل لـ Zustand أو Jotai؟ هذا الدليل العملي يفكك كل أداة في React ويشرح متى تستخدمها بناءً على حجم المشروع وأداء الذاكرة، مع أمثلة حقيقية من شركات مثل Twitter وNetflix.
في يوم من الأيام، كنت أعمل على لوحة تحكم لإدارة المحتوى في منصة تعليمية، وكان الفريق يستخدم Redux لإدارة كل شيء بدءاً من حالة زر تسجيل الدخول وحتى فلترة الجداول. بعد ثلاثة أشهر، أصبح لدينا ٤٧ أكشن و٢٣ ريدوسر، وكل تعديل بسيط يستغرق ساعات بسبب التعقيد. المشكلة؟ كنا نستخدم مطرقة ثقيلة لمسمار صغير. هذا ما دفعني لإعادة التفكير في إدارة الحالة في React: ليست كل الأدوات تناسب كل المشكلات، والاختيار الخاطئ يكلفك أداء التطبيق وساعات العمل الثمينة.
في هذا المقال، لن نتحدث عن تعريفات نظرية أو قوائم جافة للأدوات. بدلاً من ذلك، سنغوص في قلب المشكلة: كيف تختار أداة إدارة الحالة المناسبة بناءً على حجم المشروع، نوع البيانات، وأداء الذاكرة. سنشرح ما يحدث خلف الكواليس في كل أداة، متى تسبب Memory Leaks، وكيف تؤثر على الـ Event Loop. سأشارك معك أمثلة حقيقية من مشاريع عملت عليها، وأخطاء وقعت فيها، وكيفية تجنبها. لنبدأ بفهم المشكلة الأساسية التي تحلها كل هذه الأدوات.
في React، الحالة هي البيانات التي تتغير بمرور الوقت وتؤثر على ما يراه المستخدم. المشكلة تبدأ عندما تحتاج مكونات غير مرتبطة ببعضها البعض إلى مشاركة هذه البيانات. مثلاً، في تطبيق التجارة الإلكترونية، قد يحتاج شريط التنقل العلوي إلى معرفة عدد العناصر في سلة التسوق، بينما يحتاج زر الإضافة في صفحة المنتج إلى تحديث نفس العدد. إذا استخدمت props فقط، ستضطر لتمرير البيانات عبر مكونات وسيطة لا تستخدمها، وهذا ما يسمى "Prop Drilling"، وهو كابوس الصيانة.
لكن المشكلة الحقيقية ليست مجرد مشاركة البيانات، بل كيفية تحديثها بكفاءة دون إعادة رسم المكونات غير الضرورية. عندما تستخدم useState في مكون رئيسي وتمرر الحالة عبر props، فإن أي تغيير في هذه الحالة سيؤدي إلى إعادة رسم جميع المكونات التي تستقبلها، حتى لو لم تتغير البيانات التي تستخدمها فعلياً. هذا يسبب تباطؤاً ملحوظاً في التطبيقات الكبيرة، خاصة عندما تكون البيانات متداخلة ومعقدة مثل قوائم المنتجات أو بيانات المستخدمين.
// مثال على Prop Drilling السيئ
function App() {
const [cart, setCart] = useState([]);
return (
<div>
<Header cart={cart} />
<MainContent cart={cart} setCart={setCart} />
</div>
);
}
function Header({ cart }) {
return <NavBar cartCount={cart.length} />;
}
function NavBar({ cartCount }) {
return <div>سلة التسوق: {cartCount}</div>;
}
function MainContent({ cart, setCart }) {
return <ProductList cart={cart} setCart={setCart} />;
}
function ProductList({ cart, setCart }) {
// هنا نضطر لتمرير cart و setCart عبر مكونات لا تستخدمها
return <ProductItem cart={cart} setCart={setCart} />;
}الكثير من المطورين يظنون أن useState و useReducer هما أدوات بدائية لا تصلح إلا للمشاريع الصغيرة. الحقيقة هي أن هذين الـ Hooks مدهشان في معظم الحالات، وإذا استخدمت Context بحكمة، يمكنك بناء تطبيقات متوسطة الحجم بدون الحاجة لأدوات خارجية. المفتاح هنا هو فهم متى تكفي هذه الأدوات ومتى تصبح عبئاً على الأداء.
useState هو الخيار الأمثل للحالات البسيطة التي لا تحتاج لمشاركة واسعة. مثلاً، حالة زر فتح/إغلاق، أو قيمة حقل إدخال. لكن عندما تبدأ الحالة في التعقيد، مثل وجود عدة حقول مرتبطة ببعضها البعض، أو منطق تحديث معقد، يصبح useReducer هو الخيار الأفضل. الفرق الأساسي بين الاثنين هو أن useReducer يفصل منطق التحديث عن المكون، مما يجعل الكود أكثر قابلية للصيانة واختباراً.
// مثال على useReducer لإدارة حالة معقدة
const initialState = {
items: [],
filter: 'all',
loading: false,
error: null
};
function reducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
return { ...state, items: [...state.items, action.payload] };
case 'SET_FILTER':
return { ...state, filter: action.payload };
case 'FETCH_START':
return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, loading: false, items: action.payload };
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload };
default:
return state;
}
}
function ProductList() {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
dispatch({ type: 'FETCH_START' });
fetch('/api/products')
.then(res => res.json())
.then(data => dispatch({ type: 'FETCH_SUCCESS', payload: data }))
.catch(err => dispatch({ type: 'FETCH_ERROR', payload: err.message }));
}, []);
// بقية الكود...
}لكن حتى مع useReducer، لا يزال لدينا مشكلة مشاركة الحالة بين المكونات البعيدة. هنا يأتي دور Context API. الكثير من المطورين يستخدمون Context بشكل خاطئ، مما يؤدي إلى إعادة رسم جميع المكونات التي تستهلكه عند أي تغيير، حتى لو لم تستخدم البيانات المتغيرة. الحل؟ تقسيم Context إلى عدة Contexts صغيرة، كل منها مسؤول عن جزء محدد من الحالة. مثلاً، بدلاً من وضع كل حالة التطبيق في Context واحد، أنشئ Context للحالة العامة، وآخر للبيانات الثابتة مثل إعدادات المستخدم، وثالث للبيانات الديناميكية مثل سلة التسوق.
// تقسيم Context لتحسين الأداء
const UserSettingsC createContext();
const CartContext = createContext();
function App() {
const [userSettings] = useState({ theme: 'dark', language: 'ar' });
const [cart, setCart] = useState([]);
return (
<UserSettingsContext.Provider value={userSettings}>
<CartContext.Provider value={{ cart, setCart }}>
<Header />
<MainContent />
</CartContext.Provider>
</UserSettingsContext.Provider>
);
}
function Header() {
const { cart } = useContext(CartContext);
const { theme } = useContext(UserSettingsContext);
// Header لن يعاد رسمه عند تغيير cart لأن UserSettingsContext لم يتغير
return <div style={{ color: theme === 'dark' ? 'white' : 'black' }}>...</div>;
}هناك ثلاث علامات واضحة على أن الأدوات المدمجة لم تعد كافية: أولاً، عندما تجد نفسك تمرر props عبر أكثر من ثلاثة مستويات من المكونات. ثانياً، عندما تصبح حالة التطبيق معقدة لدرجة أن تحديث جزء منها يتطلب كتابة الكثير من الكود المتكرر. ثالثاً، عندما تبدأ الأداء في التراجع بسبب إعادة رسم المكونات غير الضرورية. في هذه الحالات، تحتاج إلى أداة خارجية لإدارة الحالة.
لكن قبل القفز إلى Redux أو أي أداة أخرى، اسأل نفسك: هل المشكلة حقاً في إدارة الحالة، أم في بنية المكونات نفسها؟ أحياناً، يمكن حل المشكلة بإعادة تنظيم المكونات بحيث تكون البيانات أقرب إلى المكان الذي تستخدم فيه. مثلاً، بدلاً من تمرير حالة سلة التسوق من App إلى كل مكون، يمكنك جعل مكون السلة مستقلاً ويستخدم Context فقط عند الحاجة. هذا يقلل من التعقيد ويحسن الأداء بشكل كبير.
Redux هو أشهر مكتبة لإدارة الحالة في React، لكنه أيضاً أكثرها تعقيداً. الكثير من الفرق تستخدمه في مشاريع لا تحتاج إليه أبداً، فقط لأنهم سمعوا أنه "الخيار الأفضل" للمشاريع الكبيرة. الحقيقة هي أن Redux مناسب فقط لحالات محددة جداً، وإذا استخدمته في المكان الخطأ، ستجد نفسك تكتب ثلاثة أضعاف الكود لتحقيق نفس النتيجة التي يمكنك الحصول عليها باستخدام useReducer و Context.
ما يميز Redux هو قدرته على إدارة حالة معقدة جداً ومتزامنة، خاصة عندما تحتاج إلى تتبع تاريخ التغييرات (مثل التراجع والإعادة)، أو عندما تحتاج إلى مزامنة الحالة بين عدة مصادر مثل الـ WebSocket والـ Local Storage. لكن هذه المزايا تأتي بثمن: التعقيد. في Redux، تحتاج إلى كتابة أكشن، ريدوسر، ومخزن (store)، وغالباً ما تحتاج إلى مكتبات إضافية مثل Redux Thunk أو Redux Saga للتعامل مع العمليات غير المتزامنة.
// مثال بسيط على Redux - لاحظ كمية الكود المطلوبة
// actions.js
const addToCart = (item) => ({
type: 'ADD_TO_CART',
payload: item
});
// reducers.js
const initialState = {
items: [],
total: 0
};
function cartReducer(state = initialState, action) {
switch (action.type) {
case 'ADD_TO_CART':
return {
...state,
items: [...state.items, action.payload],
total: state.total + action.payload.price
};
default:
return state;
}
}
// store.js
import { createStore } from 'redux';
const store = createStore(cartReducer);
// مكون React
import { useSelector, useDispatch } from 'react-redux';
function CartButton() {
const items = useSelector(state => state.items);
const dispatch = useDispatch();
return (
<button {() => dispatch(addToCart({ id: 1, price: 10 }))}>
إضافة إلى السلة ({items.length})
</button>
);
}المشكلة الأكبر في Redux ليست كمية الكود، بل كيف يؤثر على أداء التطبيق. في كل مرة ترسل فيها أكشن، يمر Redux بكل ريدوسرز المسجلة، حتى لو لم تتغير الحالة في بعضها. هذا يعني أنه إذا كان لديك ١٠ ريدوسرز، وكل أكشن يؤثر على واحد منهم فقط، فإن Redux سيقوم بتشغيلهم جميعاً. في التطبيقات الكبيرة، هذا يمكن أن يسبب بطءاً ملحوظاً، خاصة إذا كانت بعض الـ Reducers تحتوي على منطق معقد.
استخدم Redux فقط إذا كنت بحاجة إلى واحد أو أكثر من هذه الميزات: ١) مزامنة الحالة بين عدة مصادر مثل الـ WebSocket والـ Local Storage والـ API. ٢) تتبع تاريخ التغييرات للسماح بالتراجع والإعادة. ٣) مشاركة الحالة بين عدة تطبيقات أو إطارات عمل مختلفة (مثل React و Angular). ٤) عندما يكون لديك فريق كبير وتحتاج إلى نمط موحد لإدارة الحالة يجعل الكود أكثر قابلية للتنبؤ به.
في تجربتي، رأيت Redux يسبب مشاكل أكثر مما يحل في المشاريع الصغيرة والمتوسطة. مثلاً، في منصة تعليمية عملت عليها، استخدمنا Redux لإدارة حالة المستخدم، الدروس، والتقدم. بعد ستة أشهر، أصبح لدينا ٣٧ أكشن و١٤ ريدوسر، وكل تعديل بسيط يستغرق ساعات بسبب التعقيد. عندما انتقلنا إلى Zustand، قلصنا الكود إلى الثلث وحصلنا على أداء أفضل. Redux أداة قوية، لكنها ليست الحل لكل مشكلة.
Zustand هي مكتبة إدارة حالة خفيفة الوزن أصبحت المفضلة لدي في السنوات الأخيرة. ما يميزها هو بساطتها وقوتها في نفس الوقت. مع Zustand، يمكنك إنشاء مخازن حالة (stores) بسهولة دون الحاجة لكتابة أكشنات أو ريدوسرز. المكتبة تستخدم نمط مشابه لـ Redux لكنها تلغي الكثير من التعقيد، مما يجعلها مثالية للمشاريع المتوسطة والكبيرة التي لا تحتاج لكل مزايا Redux.
أحد أكبر مزايا Zustand هو قدرته على التعامل مع الحالة المحلية والعامة في نفس الوقت. يمكنك إنشاء مخزن حالة يستخدمه مكون واحد فقط، أو مخزن مشترك بين عدة مكونات. المكتبة أيضاً تدعم Middleware، مما يسمح لك بإضافة وظائف مثل تسجيل التغييرات أو التعامل مع العمليات غير المتزامنة بسهولة. والأفضل من ذلك، Zustand لا يسبب إعادة رسم المكونات غير الضرورية، لأنه يستخدم نظام اشتراكات ذكي.
// مثال على Zustand - لاحظ البساطة مقارنة بـ Redux
import create from 'zustand';
const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item],
total: state.total + item.price
})),
removeItem: (id) => set((state) => ({
items: state.items.filter(item => item.id !== id),
total: state.items.reduce((sum, item) => sum + item.price, 0) -
state.items.find(item => item.id === id).price
})),
total: 0
}));
function CartButton() {
const { items, addItem } = useCartStore();
return (
<button {() => addItem({ id: 1, price: 10 })}>
إضافة إلى السلة ({items.length})
</button>
);
}
function CartSummary() {
const { total } = useCartStore();
return <div>الإجمالي: {total} ريال</div>;
}ما يجعل Zustand مميزاً حقاً هو كيفية تعامله مع الذاكرة والأداء. على عكس Redux، الذي يعيد تشغيل جميع الـ Reducers عند أي تغيير، Zustand يسمح لك بتحديد بالضبط أي جزء من الحالة تريد الاشتراك فيه. هذا يعني أن المكونات لن تعاد رسمها إلا إذا تغير الجزء الذي تستخدمه من الحالة. مثلاً، في المثال السابق، مكون CartButton لن يعاد رسمه إذا تغير total فقط، لأنه لا يستخدم هذه القيمة.
اختر Zustand إذا كنت تريد: ١) مكتبة خفيفة وسهلة الاستخدام دون تعقيد Redux. ٢) القدرة على إنشاء مخازن حالة متعددة بسهولة. ٣) أداء أفضل في التطبيقات الكبيرة بسبب نظام الاشتراكات الذكي. ٤) التعامل مع الحالة المحلية والعامة في نفس الوقت. ٥) كتابة أقل للكود مع نفس القدر من الوظائف.
في مشروع حديث عملت عليه، استخدمنا Zustand لإدارة حالة تطبيق إدارة المشاريع. كان لدينا مخزن للمهام، مخزن للمستخدمين، ومخزن للإعدادات. بفضل Zustand، استطعنا تقليل كمية الكود بنسبة ٦٠٪ مقارنة بما كنا سنكتبه باستخدام Redux، وحصلنا على أداء أفضل لأن المكونات كانت تعاد رسمها فقط عند الحاجة. Zustand ليس حلاً سحرياً، لكنه الخيار الأمثل للمشاريع التي تحتاج إلى توازن بين البساطة والقوة.
عندما تبدأ الحالة في التطبيق في التعقيد بشكل كبير، مثل وجود بيانات متداخلة تعتمد على بعضها البعض، أو عندما تحتاج إلى مشاركة أجزاء صغيرة من الحالة بين مكونات بعيدة، تصبح الأدوات التقليدية مثل Redux و Zustand غير كافية. هنا تأتي المكتبات التي تستخدم نمط إدارة الحالة الذرية، مثل Jotai و Recoil. هذه المكتبات تسمح لك بتقسيم الحالة إلى وحدات صغيرة تسمى "ذرات" (atoms)، ويمكنك إنشاء علاقات بين هذه الذرات بسهولة.
الفكرة الأساسية وراء الإدارة الذرية هي أن كل جزء صغير من الحالة (مثل قيمة حقل إدخال، أو حالة زر) يكون مستقلاً ويمكن مشاركته بسهولة. عندما يتغير جزء من الحالة، فقط المكونات التي تستخدم هذا الجزء ستعاد رسمها. هذا يحسن الأداء بشكل كبير في التطبيقات الكبيرة، خاصة تلك التي تحتوي على الكثير من البيانات المتداخلة. مثلاً، في تطبيق تحرير النصوص، قد يكون لديك ذرة للحالة العامة للمحرر، وذرة لكل فقرة، وذرة لكل كلمة، وهكذا.
// مثال على Jotai لإدارة حالة ذرية
import { atom, useAtom } from 'jotai';
// تعريف الذرات
const textAtom = atom('');
const wordCountAtom = atom((get) => get(textAtom).split(' ').length);
const charCountAtom = atom((get) => get(textAtom).length);
function TextEditor() {
const [text, setText] = useAtom(textAtom);
return (
<div>
<textarea
value={text}
{(e) => setText(e.target.value)}
/>
<WordCount />
<CharCount />
</div>
);
}
function WordCount() {
const [wordCount] = useAtom(wordCountAtom);
return <div>عدد الكلمات: {wordCount}</div>;
}
function CharCount() {
const [charCount] = useAtom(charCountAtom);
return <div>عدد الأحرف: {charCount}</div>;
}ما يميز Jotai و Recoil هو كيفية تعاملهما مع الاعتمادات بين الذرات. في المثال السابق، ذرة wordCount تعتمد على ذرة textAtom. عندما يتغير textAtom، فقط المكونات التي تستخدم wordCountAtom ستعاد رسمها، وليس كل المكونات التي تستخدم textAtom. هذا يختلف عن Zustand و Redux، حيث يتعين عليك إدارة هذه الاعتمادات يدوياً، وغالباً ما ينتهي بك الأمر بإعادة رسم مكونات غير ضرورية.
استخدم Jotai أو Recoil إذا كنت تعمل على: ١) تطبيقات تحتوي على الكثير من البيانات المتداخلة والمعتمدة على بعضها البعض. ٢) مشاريع تحتاج إلى مشاركة أجزاء صغيرة من الحالة بين مكونات بعيدة دون Prop Drilling. ٣) تطبيقات تحرير نصوص أو رسومات، حيث تحتاج إلى إدارة حالة لكل عنصر على حدة. ٤) مشاريع تريد فيها تحسين الأداء إلى أقصى حد عن طريق تقليل إعادة الرسم غير الضرورية.
في شركة ناشئة عملت معها، استخدمنا Jotai لبناء محرر رسوم بيانية معقد. كان لدينا ذرات لكل شكل، ولون، وحجم، وعلاقات بين الأشكال. بفضل Jotai، استطعنا إدارة هذه الحالة المعقدة بسهولة، وكان الأداء ممتازاً حتى مع مئات الأشكال على الشاشة. الأدوات الذرية ليست للحلول البسيطة، لكنها تغير قواعد اللعبة في المشاريع المعقدة حقاً.
بعد كل هذا الشرح، قد تشعر بالحيرة: أي أداة تختار؟ الحقيقة هي أنه لا يوجد حل واحد يناسب الجميع، لكن هناك خريطة قرار بسيطة يمكنك اتباعها. أولاً، اسأل نفسك عن حجم المشروع: إذا كان صغيراً أو متوسطاً، ابدأ بـ useState و useReducer مع Context. إذا كان كبيراً ومعقداً، فكر في Zustand أو Jotai. فقط إذا كنت بحاجة إلى مزايا Redux المحددة (مثل التراجع والإعادة أو مزامنة الحالة بين عدة مصادر)، استخدم Redux.
ثانياً، فكر في نوع البيانات: إذا كانت البيانات بسيطة ومستقلة، استخدم useState أو Zustand. إذا كانت البيانات متداخلة وتعتمد على بعضها البعض، فكر في Jotai أو Recoil. ثالثاً، فكر في الفريق: إذا كان الفريق كبيراً وتحتاج إلى نمط موحد، قد يكون Redux هو الخيار الأفضل رغم تعقيده. إذا كان الفريق صغيراً ويريد كتابة أقل للكود، فاختر Zustand أو Jotai.
بعد عشر سنوات في تطوير الويب، وخوض معارك إدارة الحالة في مشاريع تتراوح من تطبيقات صغيرة إلى منصات ضخمة، خلاصة تجربتي هي هذه: ابدأ بالبساطة دائماً. استخدم useState و Context أولاً، وعندما تشعر أن الكود أصبح معقداً أو بطيئاً، انتقل إلى أداة أكثر قوة. لا تختر الأداة بناءً على شعبيتها أو ما تسمعه في المؤتمرات، بل اخترها بناءً على احتياجات مشروعك الحقيقي.
الخطأ الأكبر الذي يقع فيه المطورون هو المبالغة في هندسة الحلول منذ البداية. لا تستخدم Redux لمجرد أن "كل المشاريع الكبيرة تستخدمه"، ولا تستخدم Jotai لأن "البيانات أصبحت معقدة قليلاً". ابدأ بالحل الأبسط، وعندما يصبح هذا الحل عبئاً، قم بالترقية. إدارة الحالة في React ليست عن الأدوات، بل عن فهم المشكلة التي تحاول حلها وكيفية حلها بأقل قدر من التعقيد.