من useState إلى Redux وZustand وJotai: متى تستخدم كل أداة؟ وكيف تختار دون أن تخسر الأداء أو صحتك العقلية؟ دليل عملي بلا تعقيد.
تفتح مشروعك الجديد في React، وتبدأ بكتابة مكون بسيط. بعد ساعة تجد نفسك أمام ١٥ ملفاً مختلفاً، حالة تتناقل بين ٧ مكونات، وuseEffect يتكرر في كل مكان وكأنك تكتب جافاسكريبت عام ٢٠١٥. المشكلة ليست في React نفسها، بل في أنك لم تختر الأداة المناسبة لإدارة الحالة منذ البداية. في هذا الدليل، سنفكك كل خيار متاح، ونشرح بالضبط متى تستخدم ماذا، دون أن نخوض في نظريات أكاديمية أو أمثلة تافهة مثل عداد النقرات.
الحقيقة هي أن معظم المطورين يختارون مكتبة إدارة الحالة بناءً على الشعبية أو ما قرأوه في آخر تغريدة، وليس بناءً على احتياجات المشروع الفعلية. مثلاً، Redux أصبح مرادفاً لـ "إدارة الحالة" حتى في المشاريع الصغيرة التي لا تحتاج أكثر من useState. بينما Zustand، التي تحل نفس المشكلة بأقل من ١ كيلوبايت، لا تزال غامضة للكثيرين. سنبدأ من الأساسيات التقنية، ثم نتعمق في كل أداة، ونرى كيف تعمل خلف الكواليس في الذاكرة والمعالج.
useState هو الخط الأول للدفاع في React. عندما تكون الحالة محصورة داخل مكون واحد ولا تحتاج إلى مشاركتها مع مكونات أخرى، فلا داعي لتعقيد الأمور. لكن حتى هنا، هناك تفاصيل مهمة. مثلاً، عندما تقوم بتحديث الحالة باستخدام دالة، React لا يقوم بتحديثها فوراً، بل يضعها في قائمة الانتظار (queue) ليتم معالجتها في الدورة القادمة من الـ Event Loop. هذا يعني أنك إذا قمت بكتابة:
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
setCount(count + 1); // لن يعمل كما تتوقع
};ستجد أن القيمة زادت مرة واحدة فقط، لأن React يجمع التحديثات المتشابهة في دفعة واحدة (batch) لتحسين الأداء. الحل هو استخدام الدالة المغلفة (updater function) التي تأخذ الحالة السابقة كوسيط:
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1); // الآن ستزيد مرتينلكن حتى useState له حدوده. عندما تبدأ الحالة في النمو وتصبح معقدة، أو تحتاج إلى مشاركتها بين مكونات بعيدة في الشجرة، ستجد نفسك تستخدم props drilling بشكل مفرط. هنا يأتي دور الحلول الأخرى.
useReducer هو الخطوة التالية عندما تجد نفسك تكتب useState بشكل متكرر مع منطق تحديث معقد. فكر في useReducer كآلة حالة (state machine) مصغرة داخل مكونك. بدلاً من كتابة عدة useState وتحديثات مشروطة، يمكنك كتابة reducer واحد يعالج كل الحالات الممكنة. مثلاً، إذا كنت تبني نموذج تسجيل دخول، قد تحتاج إلى حالة تحتوي على الحقول التالية:
بدلاً من إدارة كل حالة على حدة، يمكنك استخدام useReducer لتوحيد المنطق:
const initialState = {
email: '',
password: '',
isLoading: false,
error: null,
success: false
};
function reducer(state, action) {
switch (action.type) {
case 'SET_EMAIL':
return { ...state, email: action.payload };
case 'SET_PASSWORD':
return { ...state, password: action.payload };
case 'SUBMIT_START':
return { ...state, isLoading: true, error: null };
case 'SUBMIT_SUCCESS':
return { ...state, isLoading: false, success: true };
case 'SUBMIT_ERROR':
return { ...state, isLoading: false, error: action.payload };
default:
return state;
}
}
const [state, dispatch] = useReducer(reducer, initialState);الميزة هنا ليست فقط في التنظيم، بل في الأداء أيضاً. عندما تقوم بتحديث الحالة باستخدام useReducer، React يعرف بالضبط ما تغير، ويمكنه تحسين إعادة الرسم (re-render) بشكل أفضل من useState في الحالات المعقدة. لكن حتى useReducer له حدوده: إذا احتجت إلى مشاركة هذه الحالة بين مكونات بعيدة، ستضطر إلى رفع الحالة إلى مكون مشترك، مما قد يؤدي إلى إعادة رسم غير ضرورية للمكونات الوسيطة.
Context API هو الحل المدمج في React لمشاركة الحالة بين مكونات بعيدة دون الحاجة إلى تمرير props عبر كل مستوى. الفكرة بسيطة: تضع الحالة في Context، ثم تستهلكها في أي مكون داخل الشجرة دون الحاجة إلى تمريرها يدوياً. لكن هنا تكمن المشكلة: Context ليس مصمماً لإدارة الحالة الديناميكية بكفاءة. عندما يتغير قيمة Context، كل المكونات التي تستهلكه ستعيد الرسم، حتى لو لم تستخدم القيمة المتغيرة.
لنأخذ مثالاً عملياً: تخيل أنك تبني لوحة تحكم تحتوي على شريط جانبي وقائمة مهام. الشريط الجانبي يحتوي على إعدادات المستخدم، والقائمة تعرض المهام بناءً على هذه الإعدادات. إذا استخدمت Context لمشاركة إعدادات المستخدم، فسيتم إعادة رسم القائمة في كل مرة يتغير فيها إعداد واحد، حتى لو كان التغيير في إعداد لا يؤثر على القائمة. هذا يمكن أن يؤدي إلى مشاكل أداء حقيقية في التطبيقات الكبيرة.
// إنشاء Context
const UserSettingsC createContext();
// Provider
<UserSettingsContext.Provider value={settings}>
<Sidebar />
<TaskList />
</UserSettingsContext.Provider>
// استهلاك في Sidebar
const settings = useContext(UserSettingsContext);
// استهلاك في TaskList
const settings = useContext(UserSettingsContext); // إعادة رسم حتى لو تغير إعداد لا علاقة له بالقائمةالحل هنا هو تقسيم Context إلى عدة Contexts أصغر، بحيث يستهلك كل مكون فقط ما يحتاجه. مثلاً، يمكنك إنشاء Context منفصل لإعدادات المستخدم وآخر لقائمة المهام. لكن حتى هذا الحل ليس مثالياً، لأنه يضيف تعقيداً ويجعل الكود أصعب في الصيانة. في رأيي، Context API مفيد للمعلومات الثابتة نسبياً مثل إعدادات التطبيق أو معلومات المستخدم، وليس للحالة الديناميكية التي تتغير بشكل متكرر.
Redux هو الحل الأشهر لإدارة الحالة في React، لكنه أيضاً الأكثر تعقيداً. الفكرة الأساسية في Redux هي وجود متجر مركزي (store) يحتوي على كل حالة التطبيق، ويتم تحديث هذه الحالة باستخدام actions وreducers نقية. الميزة الرئيسية لـ Redux هي القدرة على تتبع كل تغيير في الحالة، مما يجعل تصحيح الأخطاء (debugging) أسهل بكثير. لكن الثمن هو التعقيد: عليك كتابة الكثير من الكود حتى لإجراء تغيير بسيط.
لنأخذ مثالاً: تريد إضافة عنصر إلى قائمة المهام. في Redux، ستحتاج إلى:
// Action Types
const ADD_TASK = 'ADD_TASK';
// Action Creator
export const addTask = (task) => ({
type: ADD_TASK,
payload: task
});
// Reducer
function tasksReducer(state = [], action) {
switch (action.type) {
case ADD_TASK:
return [...state, action.payload];
default:
return state;
}
}
// في المكون
const dispatch = useDispatch();
const tasks = useSelector(state => state.tasks);
dispatch(addTask({ id: 1, text: 'Learn Redux' }));هذا الكود يبدو مبالغاً فيه لمهمة بسيطة، لكنه يصبح مفيداً عندما يكون لديك تطبيق كبير مع حالة معقدة تحتاج إلى مشاركة بين عدة مكونات. الميزة الحقيقية لـ Redux تظهر عندما تحتاج إلى ميزات متقدمة مثل middleware (لإجراء عمليات جانبية مثل استدعاءات API)، أو عندما تريد تتبع كل تغيير في الحالة باستخدام أدوات مثل Redux DevTools. لكن في التطبيقات الصغيرة والمتوسطة، Redux يضيف تعقيداً غير ضروري.
في تجربتي، رأيت الكثير من المشاريع تستخدم Redux فقط لأن "الجميع يستخدمونه"، ثم ينتهي بهم الأمر بكود معقد وصعب الصيانة. إذا كنت تفكر في استخدام Redux، اسأل نفسك: هل حقاً أحتاج إلى كل هذه الميزات، أم أنني أبحث عن حل أبسط؟
Zustand هو مكتبة إدارة حالة حديثة تحاول الجمع بين بساطة useState وقوة Redux، دون التعقيد. الفكرة الأساسية في Zustand هي وجود متجر مركزي، لكن بدلاً من استخدام actions وreducers، يمكنك تحديث الحالة مباشرة كما تفعل مع useState. هذا يجعل الكود أقصر وأكثر قابلية للقراءة، مع الحفاظ على الأداء العالي.
لنرى كيف يمكن تنفيذ نفس مثال قائمة المهام باستخدام Zustand:
import create from 'zustand';
const useStore = create((set) => ({
tasks: [],
addTask: (task) => set((state) => ({ tasks: [...state.tasks, task] })),
removeTask: (id) => set((state) => ({ tasks: state.tasks.filter(t => t.id !== id) })),
}));
// في المكون
const { tasks, addTask } = useStore();
addTask({ id: 1, text: 'Learn Zustand' });هذا الكود أقصر بكثير من Redux، لكنه يحقق نفس النتيجة. الميزة الكبيرة لـ Zustand هي أنه يسمح لك بتحديد أي جزء من الحالة تريد الاشتراك فيه، مما يقلل من إعادة الرسم غير الضرورية. مثلاً، يمكنك كتابة:
const tasks = useStore(state => state.tasks); // إعادة رسم فقط عند تغيير tasksهذا يشبه استخدام useSelector في Redux، لكنه أكثر بساطة. Zustand يدعم أيضاً middleware، ويمكنك استخدامه لإجراء عمليات جانبية مثل استدعاءات API. في رأيي، Zustand هو الخيار الأفضل لمعظم التطبيقات التي تحتاج إلى إدارة حالة مركزية دون التعقيد الذي يأتي مع Redux.
Jotai وRecoil هما مكتبتان تستخدمان مفهوم "الحالة الذرية" (Atomic State) لإدارة الحالة. الفكرة هنا هي تقسيم الحالة إلى وحدات صغيرة (atoms)، ويمكن للمكونات الاشتراك في الذرات التي تحتاجها فقط. هذا يقلل من إعادة الرسم غير الضرورية ويجعل إدارة الحالة أكثر كفاءة في التطبيقات الكبيرة والمعقدة.
لنأخذ مثالاً باستخدام Jotai:
import { atom, useAtom } from 'jotai';
// تعريف ذرة
const tasksAtom = atom([]);
const filterAtom = atom('all');
// في المكون
const [tasks, setTasks] = useAtom(tasksAtom);
const [filter, setFilter] = useAtom(filterAtom);
setTasks([...tasks, { id: 1, text: 'Learn Jotai' }]);الميزة هنا هي أن المكونات التي تستخدم tasksAtom فقط ستعيد الرسم عند تغيير tasks، بينما المكونات التي تستخدم filterAtom فقط ستعيد الرسم عند تغيير filter. هذا يجعل الأداء أفضل بكثير من Context API في التطبيقات الكبيرة. Jotai وRecoil يدعمان أيضاً ميزات متقدمة مثل الذرات المشتقة (derived atoms) والعمليات غير المتزامنة (async atoms).
لكن حتى هذه المكتبات لها حدودها. فهي تضيف تعقيداً إضافياً مقارنة بـ Zustand، وقد لا تكون ضرورية في التطبيقات الصغيرة والمتوسطة. في رأيي، Jotai وRecoil هما الخياران الأمثل للمشاريع الكبيرة التي تحتاج إلى إدارة حالة معقدة مع أداء عالي، مثل لوحات التحكم أو التطبيقات التي تحتوي على العديد من المكونات المترابطة.
بعد استعراض كل هذه الخيارات، قد تشعر بالحيرة: متى تستخدم أياً منها؟ إليك دليلي السريع بناءً على تجربتي:
لكن تذكر: لا توجد قاعدة صارمة. أحياناً قد تحتاج إلى استخدام أكثر من أداة في نفس المشروع. مثلاً، يمكنك استخدام Zustand للحالة العامة، وuseState للحالة المحلية، وContext API لإعدادات التطبيق. المفتاح هو فهم احتياجات مشروعك واختيار الأداة المناسبة بناءً على ذلك، وليس بناءً على الشعبية أو العادة.
إذا كنت تبدأ مشروعاً جديداً، ابدأ بأبسط حل ممكن: useState وuseReducer. عندما تجد نفسك تستخدم props drilling بشكل مفرط، انتقل إلى Zustand. إذا كنت بحاجة إلى ميزات متقدمة مثل تتبع الحالة أو middleware، فكر في Redux. وإذا كان مشروعك كبيراً ومعقداً وتحتاج إلى أداء عالي، جرب Jotai أو Recoil. لا تختر أداة إدارة الحالة بناءً على ما قرأته في آخر تغريدة، بل بناءً على احتياجات مشروعك الفعلية. وفي النهاية، تذكر أن الهدف هو كتابة كود سهل الصيانة وأداء عالي، وليس استخدام أحدث الأدوات فقط.