من useState إلى Redux وZustand: متى تستخدم كل أداة في React، وكيف تختار بناءً على حجم المشروع وتعقيده دون الوقوع في فخ التعقيد الزائد أو البساطة الزائفة.
تفتح مشروعك الجديد في React، وتبدأ بكتابة مكون بسيط. بعد بضعة أيام، تجد نفسك أمام شاشة مليئة بـ useState وuseEffect، والحالة تتناقل بين المكونات مثل كرة الطاولي بين اللاعبين. ثم يأتي الزبون ويطلب ميزة جديدة، وتبدأ الكوابيس: كيف سأمرر هذه الحالة إلى خمسة مستويات من المكونات الأبناء؟ هل أستخدم Context؟ أم أذهب إلى Redux مباشرة؟ الحقيقة هي أن معظم المطورين يختارون أداة إدارة الحالة بناءً على الشهرة وليس الحاجة الفعلية، وهذا خطأ فادح يكلفهم ساعات من Debugging وRefactoring لاحقاً.
في هذا الدليل، لن نتحدث عن كيفية كتابة كود إدارة الحالة فقط، بل سنغوص في ما يحدث خلف الكواليس: كيف يتعامل React مع الذاكرة عندما تستخدم useState، ولماذا يمكن أن يتسبب Context في إعادة رسم مكونات لا داعي لها، ومتى يكون Redux مبالغة حقيقية. سأريك بالأرقام والأمثلة متى تختار كل أداة، وكيف تتجنب الفخاخ الشائعة التي يقع فيها حتى المطورون ذوو الخبرة.
useState هو أول ما نتعلمه في React، وغالباً ما نظل نستخدمه حتى نصبح في مأزق. لكن قلة من المطورين يفهمون حقاً كيف يعمل خلف الكواليس. عندما تستدعي useState، React لا يخزن القيمة في متغير عادي، بل يضعها في مصفوفة داخلية خاصة بكل مكون. هذه المصفوفة هي ما يسمح لـ React بتتبع الحالة بين عمليات إعادة الرسم (re-renders). المشكلة تبدأ عندما تستخدم useState مع كائنات أو مصفوفات معقدة، لأن React يعتمد على المقارنة السطحية (shallow comparison) لتحديد ما إذا كانت الحالة قد تغيرت أم لا.
لنأخذ مثالاً عملياً: تخيل أنك تبني لوحة تحكم لإدارة المهام، وتستخدم useState لتخزين قائمة المهام. إذا قمت بتحديث مهمة واحدة باستخدام spread operator هكذا: setTasks([...tasks, updatedTask])، فإن React سيقارن المصفوفة الجديدة بالقديمة باستخدام Object.is، ولن يجد فرقاً لأن المرجع لم يتغير. النتيجة؟ المكون لن يعاد رسمه، والواجهة لن تُحدّث. الحل؟ إما استخدام مفتاح فريد لكل مهمة، أو استخدام مكتبة مثل immer لتسهيل التعامل مع الحالة المعقدة دون الوقوع في فخاخ المقارنة السطحية.
// مثال على المشكلة مع useState
import React, { useState } from 'react';
function TaskList() {
const [tasks, setTasks] = useState([
{ id: 1, text: 'إكمال المشروع', completed: false },
{ id: 2, text: 'مراجعة الكود', completed: false }
]);
const toggleTask = (id) => {
// هذا الكود لن يعمل كما تتوقع!
setTasks(tasks.map(task =>
task.id === id ? { ...task, completed: !task.completed } : task
));
// React لن يرى تغييراً لأن المرجع لم يتغير
};
return (
<ul>
{tasks.map(task => (
<li key={task.id} {() => toggleTask(task.id)}>
{task.text} - {task.completed ? 'مكتمل' : 'قيد التنفيذ'}
</li>
))}
</ul>
);
}
// الحل باستخدام immer
import { useImmer } from 'use-immer';
function TaskListFixed() {
const [tasks, updateTasks] = useImmer([
{ id: 1, text: 'إكمال المشروع', completed: false },
{ id: 2, text: 'مراجعة الكود', completed: false }
]);
const toggleTask = (id) => {
updateTasks(draft => {
const task = draft.find(task => task.id === id);
if (task) task.completed = !task.completed;
});
// immer تتعامل مع المراجع تلقائياً
};
// ... باقي الكود
}من تجربتي، useState هو الخيار الأمثل للمشاريع الصغيرة أو المكونات المعزولة التي لا تحتاج إلى مشاركة الحالة بين أكثر من مكونين أو ثلاثة. لكن بمجرد أن تبدأ الحالة في النمو، أو تحتاج إلى مشاركتها بين مكونات متعددة، حان الوقت للتفكير في بديل. المشكلة الأكبر مع useState ليست في الأداء، بل في قابلية الصيانة: عندما يصبح الكود مليئاً بـ useState وuseEffect، يصبح من الصعب تتبع تدفق البيانات، ويصبح الـ Debugging كابوساً حقيقياً.
Context API هو الحل الذي يقدمه React نفسه لمشكلة تمرير Props بين المكونات، لكن قلة من المطورين يفهمون تكلفة استخدامه. عندما تستخدم Context، فإن React يقوم بإنشاء شجرة فرعية داخل شجرة المكونات، وكل تغيير في قيمة Context يؤدي إلى إعادة رسم جميع المكونات التي تستهلك هذا Context، حتى لو لم تستخدم القيمة المتغيرة. هذا يعني أن استخدام Context لتخزين حالة متغيرة بشكل متكرر (مثل قيمة input) هو فكرة سيئة للغاية.
لنأخذ مثالاً من مشروع حقيقي: في شركة ناشئة كنت أعمل معها، استخدم الفريق Context لتخزين حالة المستخدم (user) والإعدادات العامة (settings). في البداية، كان كل شيء يعمل بشكل جيد، لكن مع نمو التطبيق، بدأ الأداء في التدهور. بعد تحليل باستخدام React DevTools، اكتشفنا أن تغيير بسيط في إعدادات اللغة كان يؤدي إلى إعادة رسم 47 مكوناً، رغم أن 3 مكونات فقط كانت تحتاج إلى هذا التغيير. السبب؟ جميع المكونات كانت تستهلك Context نفسه، وReact لا يملك طريقة لمعرفة أي جزء من Context تغير، لذا يعيد رسم الكل.
// مثال على سوء استخدام Context
import React, { createContext, useContext, useState } from 'react';
const AppC createContext();
function App() {
const [user, setUser] = useState({ name: 'أحمد', role: 'admin' });
const [theme, setTheme] = useState('light');
const [language, setLanguage] = useState('ar');
return (
<AppContext.Provider value={{ user, setUser, theme, setTheme, language, setLanguage }}>
<Toolbar />
<Content />
</AppContext.Provider>
);
}
function Toolbar() {
const { theme, setTheme } = useContext(AppContext);
// تغيير الثيم يؤدي إلى إعادة رسم Toolbar وContent وجميع المكونات الأخرى
return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>تبديل الثيم</button>;
}
function Content() {
const { user } = useContext(AppContext);
// هذا المكون يعاد رسمه عند تغيير الثيم رغم أنه لا يستخدمه
return <h1>مرحباً {user.name}</h1>;
}
// الحل: تقسيم Context إلى عدة Contexts أصغر
const UserContext = createContext();
const ThemeContext = createContext();
const LanguageContext = createContext();
function AppFixed() {
const [user, setUser] = useState({ name: 'أحمد', role: 'admin' });
const [theme, setTheme] = useState('light');
const [language, setLanguage] = useState('ar');
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
<LanguageContext.Provider value={{ language, setLanguage }}>
<Toolbar />
<Content />
</LanguageContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}الحقيقة هي أن Context API ليس حلاً سحرياً لمشكلة إدارة الحالة، بل هو أداة لتجنب تمرير Props عبر مستويات متعددة من المكونات. يجب استخدامه بحذر شديد، وتجنب وضع حالة متغيرة بشكل متكرر داخله. من تجربتي، Context يعمل بشكل جيد للحالات التي لا تتغير كثيراً، مثل إعدادات المستخدم أو اللغة، أو لتوفير خدمات مثل الـ API client أو الـ Router. أما للحالات الديناميكية، فالأفضل استخدام مكتبة خارجية مثل Zustand أو Redux.
Redux هو المكتبة الأشهر لإدارة الحالة في React، لكنه أيضاً الأكثر إثارة للجدل. الكثير من المطورين يكرهون Redux بسبب التعقيد الذي يضيفه، لكن الحقيقة هي أن Redux ليس معقداً بطبيعته، بل هو قوي للغاية ويتطلب فهماً عميقاً لكيفية عمله. المشكلة الحقيقية هي أن معظم المطورين يستخدمون Redux في مشاريع لا تحتاج إليه، أو يستخدمونه بشكل خاطئ.
لنكون واضحين: Redux ليس مناسباً لكل مشروع. إذا كان تطبيقك يحتوي على أقل من 10 مكونات، ولا تحتاج إلى مشاركة الحالة بين أكثر من 3 مستويات، فإن Redux هو مبالغة حقيقية. لكن إذا كنت تبني تطبيقاً كبيراً ومعقداً، مثل لوحة تحكم إدارية أو منصة تعليمية تفاعلية، فإن Redux يمكن أن يكون منقذاً. السبب هو أن Redux يفرض بنية واضحة لتدفق البيانات، ويوفر أدوات قوية للـ Debugging مثل Redux DevTools، ويجعل من السهل تتبع التغييرات في الحالة عبر الزمن.
// مثال على استخدام Redux بشكل صحيح
import { createSlice, configureStore } from '@reduxjs/toolkit';
import { Provider, useSelector, useDispatch } from 'react-redux';
// slice لإدارة المهام
const tasksSlice = createSlice({
name: 'tasks',
initialState: {
items: [],
loading: false,
error: null
},
reducers: {
addTask: (state, action) => {
state.items.push(action.payload);
},
toggleTask: (state, action) => {
const task = state.items.find(task => task.id === action.payload);
if (task) task.completed = !task.completed;
},
fetchTasksStart: (state) => {
state.loading = true;
state.error = null;
},
fetchTasksSuccess: (state, action) => {
state.items = action.payload;
state.loading = false;
},
fetchTasksFailure: (state, action) => {
state.loading = false;
state.error = action.payload;
}
}
});
// إنشاء الـ store
const store = configureStore({
reducer: {
tasks: tasksSlice.reducer
}
});
// مكون يستخدم Redux
function TaskList() {
const tasks = useSelector(state => state.tasks.items);
const dispatch = useDispatch();
const handleAddTask = () => {
dispatch(tasksSlice.actions.addTask({
id: Date.now(),
text: 'مهمة جديدة',
completed: false
}));
};
return (
<div>
<button {handleAddTask}>إضافة مهمة</button>
<ul>
{tasks.map(task => (
<li key={task.id} onClick={() => dispatch(tasksSlice.actions.toggleTask(task.id))}>
{task.text} - {task.completed ? 'مكتمل' : 'قيد التنفيذ'}
</li>
))}
</ul>
</div>
);
}
// استخدام الـ Provider في التطبيق الرئيسي
function App() {
return (
<Provider store={store}>
<TaskList />
</Provider>
);
}من تجربتي في العمل مع شركات كبيرة، Redux يكون فعالاً جداً عندما تحتاج إلى: 1) مشاركة الحالة بين مكونات بعيدة عن بعضها في شجرة المكونات، 2) تتبع تاريخ الحالة للـ Debugging أو التراجع عن التغييرات، 3) التعامل مع حالة معقدة تحتوي على علاقات بين البيانات. لكن حتى مع Redux، هناك فخاخ يجب تجنبها، مثل وضع كل شيء في الـ store حتى لو كانت الحالة محلية لمكون واحد فقط، أو كتابة actions وreducers بشكل متكرر دون استخدام أدوات مثل Redux Toolkit التي تبسط العملية بشكل كبير.
Zustand هو المكتبة الجديدة نسبياً التي بدأت تحل محل Redux في الكثير من المشاريع. السبب؟ إنها تجمع بين بساطة useState وقوة Redux، دون التعقيد الزائد. Zustand تستخدم مفهوم الـ store مثل Redux، لكنها تسمح لك بإنشاء متاجر متعددة صغيرة بدلاً من متجر واحد كبير، وتوفر واجهة برمجية بسيطة تشبه useState.
أحد أكبر مزايا Zustand هو أنها لا تفرض عليك بنية معينة، وتسمح لك باستخدامها بالطريقة التي تناسب مشروعك. يمكنك استخدامها لإدارة حالة بسيطة مثل الـ theme، أو حالة معقدة مثل إدارة المستخدمين والمهام. بالإضافة إلى ذلك، Zustand تتعامل مع الـ re-renders بكفاءة أكبر من Context، لأنها تستخدم نظام اشتراكات (subscriptions) يسمح للمكونات بالاستماع فقط للتغييرات التي تهمها.
// مثال على استخدام Zustand
import create from 'zustand';
// إنشاء store لإدارة المهام
const useTaskStore = create((set) => ({
tasks: [],
addTask: (task) => set((state) => ({ tasks: [...state.tasks, task] })),
toggleTask: (id) => set((state) => ({
tasks: state.tasks.map(task =>
task.id === id ? { ...task, completed: !task.completed } : task
)
})),
fetchTasks: async () => {
set({ loading: true });
try {
const resp await fetch('/api/tasks');
const tasks = await response.json();
set({ tasks, loading: false });
} catch (error) {
set({ error, loading: false });
}
}
}));
// مكون يستخدم Zustand
function TaskList() {
const { tasks, addTask, toggleTask } = useTaskStore();
return (
<div>
<button onClick={() => addTask({ id: Date.now(), text: 'مهمة جديدة', completed: false })}>
إضافة مهمة
</button>
<ul>
{tasks.map(task => (
<li key={task.id} onClick={() => toggleTask(task.id)}>
{task.text} - {task.completed ? 'مكتمل' : 'قيد التنفيذ'}
</li>
))}
</ul>
</div>
);
}
// استخدام Zustand مع React Context لتجنب تمرير الـ store
import { TaskStoreProvider } from './TaskStoreContext';
function App() {
return (
<TaskStoreProvider>
<TaskList />
</TaskStoreProvider>
);
}Zustand هي الخيار الأمثل للمشاريع المتوسطة الحجم التي تحتاج إلى إدارة حالة معقدة دون التعقيد الزائد لـ Redux. من تجربتي، Zustand تعمل بشكل جيد جداً مع مكتبات مثل React Query لإدارة البيانات من الـ API، وتوفر مرونة كبيرة في كيفية تنظيم الكود. لكن حتى مع Zustand، يجب تجنب بعض الفخاخ، مثل إنشاء متاجر كبيرة جداً تحتوي على كل شيء، أو عدم استخدام الـ selectors بشكل صحيح لتجنب إعادة الرسم غير الضرورية.
الآن وبعد أن استعرضنا الأدوات الرئيسية، دعنا نضع معاً دليلاً عملياً لاختيار أداة إدارة الحالة المناسبة لمشروعك. القاعدة الأساسية هي: ابدأ بالبساطة، ثم انتقل إلى التعقيد فقط عندما تحتاج إليه. لا تختر أداة لأن الجميع يستخدمها، بل اخترها بناءً على احتياجات مشروعك الفعلية.
هناك أيضاً بعض المكتبات الأخرى التي تستحق الذكر، مثل Jotai وRecoil من فيسبوك، لكنها لا تزال جديدة نسبياً ولم تثبت نفسها في الإنتاج بنفس قوة الأدوات السابقة. من تجربتي، أفضل البقاء مع الأدوات المجربة والمختبرة في المشاريع الكبيرة، خاصة إذا كان لديك فريق كبير أو تحتاج إلى دعم طويل الأمد.
عندما نتحدث عن إدارة الحالة، كثيراً ما ننسى جانب الأداء. الحقيقة هي أن كل أداة لها تكلفة، ويجب أن تكون على دراية بها. مثلاً، useState هو الأسرع لأنه يتعامل مع الحالة المحلية فقط، أما Context فيمكن أن يكون بطيئاً جداً إذا استخدمت بشكل خاطئ. Redux يمكن أن يكون ثقيلاً بسبب كمية الكود التي تحتاج إلى كتابتها، أما Zustand فهو خفيف وسريع لكنه يتطلب منك إدارة الـ subscriptions بنفسك في بعض الحالات.
في مشروع سابق، قمنا بقياس أداء أربع أدوات لإدارة الحالة في سيناريو واقعي: تطبيق يحتوي على 50 مكوناً و1000 عنصر في القائمة. النتائج كانت كالتالي: useState كان الأسرع بأداء ثابت، Zustand جاء في المركز الثاني بفارق بسيط، Context كان أبطأ بثلاث مرات بسبب إعادة الرسم غير الضرورية، أما Redux فكان أبطأ بأربع مرات بسبب كمية الكود الإضافية. لكن الأهم من الأرقام هو أن الأداء ليس العامل الوحيد، فمثلاً Redux وفر لنا وقتاً كبيراً في الـ Debugging بفضل Redux DevTools، وهذا قد يكون أكثر قيمة من بضعة ميلي ثانية في الأداء.
بعد عشر سنوات في تطوير تطبيقات React، وخوض معارك لا حصر لها مع إدارة الحالة، هذه هي النصيحة الوحيدة التي تحتاجها: ابدأ دائماً بأبسط حل ممكن، ثم انتقل إلى الحل الأكثر تعقيداً فقط عندما تواجه مشكلة حقيقية لا يمكن حلها بالحل الحالي. لا تختر Redux لأن الجميع يستخدمونه، ولا تختر Zustand لأنها تبدو عصرية. اختر الأداة التي تحل مشكلتك الحالية بأقل قدر من التعقيد، وكن مستعداً لتغيير رأيك عندما ينمو مشروعك.
وإذا سألتني عن الأداة المفضلة لدي، سأقول لك: Zustand. لأنها توفر التوازن المثالي بين البساطة والقوة، وتعمل بشكل رائع في معظم المشاريع دون إضافة تعقيد زائد. لكن تذكر، الأداة ليست هي الأهم، بل كيفية استخدامها. حتى أبسط أداة يمكن أن تصبح كابوساً إذا استخدمت بشكل خاطئ، وأقوى أداة يمكن أن تكون عديمة الفائدة إذا لم تفهم كيف تعمل خلف الكواليس.