من useState إلى Redux وZustand: متى تستخدم كل أداة في إدارة الحالة في React، وكيف تختار الحل الأمثل لمشروعك دون الوقوع في فخ التعقيد الزائد أو النقص المؤلم.
تفتح مشروعك الجديد في React، وتبدأ بكتابة مكون بسيط يعرض قائمة مهام. بعد يومين، تجد نفسك أمام ١٥ مكوناً متداخلاً، و٨ حالات مختلفة، و٣ مكتبات خارجية تتنازع على نفس البيانات. هنا تبدأ المشكلة الحقيقية: كيف تدير الحالة دون أن يتحول مشروعك إلى غابة من الـ props drilling أو إلى متاهة من الـ context providers؟ الحقيقة هي أن اختيار أداة إدارة الحالة في React ليس مجرد مسألة ذوق، بل قرار هندسي يؤثر على أداء التطبيق، وصيانة الكود، وسهولة التوسع. في هذا الدليل، سنفكك كل خيار متاح، ونشرح متى يكون مناسباً، ومتى يكون كارثة تنتظر الحدوث.
لنبدأ بالحقائق الصادمة: ٦٧٪ من مشاريع React التي تفشل في التوسع تعاني من مشاكل في إدارة الحالة، وفقاً لاستبيان أجرته شركة Bit لعام ٢٠٢٣. المشكلة ليست في نقص الأدوات، بل في استخدامها في المكان الخطأ. مثلاً، استخدام Redux لإدارة حالة زر بسيط هو مثل استخدام شاحنة لنقل كوب قهوة، بينما استخدام useState لإدارة بيانات المستخدم في تطبيق كبير يشبه محاولة ملء مسبح باستخدام ملعقة شاي. سنبدأ من الأساسيات، ثم نتعمق في الأدوات المتقدمة، مع التركيز على ما يحدث خلف الكواليس في الذاكرة والمعالج.
useState هو الخط الأول للدفاع في إدارة الحالة داخل مكون React. إنه بسيط، مباشر، ولا يحتاج إلى أي إعداد مسبق. لكن لا تخطئ: بساطته تخفي وراءها آلية معقدة تعتمد على الـ closure في JavaScript وعلى خوارزمية الـ reconciliation في React. عندما تستدعي useState، فإن React لا يخزن القيمة الجديدة فقط، بل يحتفظ بمرجع للقيمة السابقة داخل الـ fiber node الخاص بالمكون. هذا يعني أن كل تحديث للحالة يؤدي إلى إنشاء نسخة جديدة من الـ state object، مما يضمن أن React قادر على مقارنة الحالة القديمة بالجديدة بكفاءة أثناء عملية الـ re-render.
لكن هنا تكمن المشكلة: useState ليس مصمماً للتعامل مع حالات معقدة أو مشتركة بين مكونات متعددة. مثلاً، إذا كان لديك مكون يعرض قائمة مهام، ومكون آخر يضيف مهام جديدة، فإن استخدام useState في كلا المكونين سيؤدي إلى حالة من الـ state duplication، حيث ستجد نفسك مضطراً لتمرير البيانات عبر الـ props بين المكونات، مما يجعل الكود صعب الصيانة ويزيد من احتمالية حدوث أخطاء. أيضاً، إذا كنت تعتمد على useState لإدارة بيانات تعتمد على قيم سابقة (مثل العدادات)، فيجب أن تكون حذراً جداً في كيفية تحديث الحالة، لأن React قد يجمع عدة تحديثات معاً في دفعة واحدة، مما يؤدي إلى نتائج غير متوقعة إذا لم تستخدم الـ functional update بشكل صحيح.
// مثال على استخدام useState بشكل صحيح مع functional update
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState([{ text: 'Learn React', completed: false }]);
// تحديث الحالة بناءً على القيمة السابقة
const increment = () => {
setCount(prevCount => prevCount + 1); // استخدام functional update لتجنب مشاكل الـ batching
};
// إضافة مهمة جديدة
const addTodo = () => {
setTodos(prevTodos => [
...prevTodos,
{ text: 'New Task', completed: false }
]);
};
return (
<div>
<p>Count: {count}</p>
<button {increment}>Increment</button>
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo.text}</li>
))}
</ul>
<button onClick={addTodo}>Add Todo</button>
</div>
);
}عندما تبدأ الحالة في مكونك في التعقيد، ويصبح من الصعب تتبع التحديثات باستخدام useState، فإن useReducer هو الحل الطبيعي. useReducer ليس مجرد بديل لـ useState، بل هو نمط تصميم يعتمد على مفهوم الـ reducer function، والذي يأتي من مكتبة Redux الشهيرة. الفكرة الأساسية هي أن تقوم بتعريف دالة reducer تأخذ الحالة الحالية وإجراءً (action)، ثم تعيد الحالة الجديدة بناءً على هذا الإجراء. هذا النمط يجعل من السهل إدارة الحالات المعقدة التي تتضمن عدة قيم مترابطة، ويجعل منطق التحديث أكثر قابلية للتنبؤ والتتبع.
من الناحية الفنية، useReducer يعمل بشكل مشابه جداً لـ useState، لكنه ينقل منطق التحديث إلى دالة خارجية. عندما تستدعي dispatch لإرسال إجراء، فإن React يمرر هذا الإجراء إلى الـ reducer function، والذي يقوم بحساب الحالة الجديدة ويعيدها. هذا يعني أن كل تحديث للحالة يمر عبر نقطة واحدة، مما يجعل من السهل تتبع التغييرات وفهم كيفية تطور الحالة مع مرور الوقت. أيضاً، لأن الـ reducer هو دالة نقية (pure function)، فإنه يمكن اختباره بسهولة دون الحاجة إلى مكونات React، مما يحسن من جودة الكود ويقلل من احتمالية حدوث أخطاء.
// مثال على استخدام useReducer لإدارة حالة معقدة
import React, { useReducer } from 'react';
// تعريف الـ reducer function
const todoReducer = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, { text: action.text, completed: false }],
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map((todo, index) =>
index === action.index ? { ...todo, completed: !todo.completed } : todo
),
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter((_, index) => index !== action.index),
};
default:
return state;
}
};
function TodoApp() {
const [state, dispatch] = useReducer(todoReducer, {
todos: [],
});
const handleSubmit = (e) => {
e.preventDefault();
const input = e.target.elements.todoInput;
dispatch({ type: 'ADD_TODO', text: input.value });
input.value = '';
};
return (
<div>
<form {handleSubmit}>
<input name="todoInput" placeholder="Add a new task" />
<button type="submit">Add</button>
</form>
<ul>
{state.todos.map((todo, index) => (
<li key={index}>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => dispatch({ type: 'TOGGLE_TODO', index })}>
Toggle
</button>
<button onClick={() => dispatch({ type: 'DELETE_TODO', index })}>
Delete
</button>
</li>
))}
</ul>
</div>
);
}useReducer ليس مجرد بديل لـ useState، بل هو أداة قوية لإدارة الحالات التي تتضمن عدة قيم مترابطة أو منطق تحديث معقد. مثلاً، إذا كنت تبني لوحة تحكم تحتوي على عدة فلترات، وحقول بحث، وحالة تحميل، فإن useReducer يمكن أن يساعد في تنظيم هذا الكود وجعل منطق التحديث أكثر وضوحاً. أيضاً، إذا كنت بحاجة إلى تحديث عدة أجزاء من الحالة بناءً على إجراء واحد، فإن useReducer يجعل هذا الأمر سهلاً ومباشراً، حيث يمكنك معالجة كل جزء من الحالة داخل الـ reducer function.
لكن useReducer ليس الحل السحري لكل المشاكل. إذا كانت حالتك بسيطة ولا تحتاج إلى منطق تحديث معقد، فإن استخدام useReducer قد يكون مبالغة غير ضرورية. أيضاً، إذا كنت بحاجة لمشاركة الحالة بين مكونات متعددة، فإن useReducer وحده لن يكون كافياً، وستحتاج إلى استخدامه مع Context API أو مكتبة خارجية مثل Redux أو Zustand. في تجربتي، أفضل استخدام useReducer عندما أجد نفسي أكتب أكثر من ٣ أو ٤ useState في مكون واحد، أو عندما يصبح منطق التحديث معقداً ويصعب تتبع التغيرات.
عندما تحتاج لمشاركة الحالة بين مكونات متعددة دون الحاجة لتمرير البيانات عبر الـ props في كل مستوى، فإن Context API هو الحل المدمج في React. الفكرة الأساسية وراء Context هي إنشاء مخزن مركزي للبيانات يمكن لأي مكون الوصول إليه دون الحاجة لتمريره عبر مكونات وسيطة. هذا يجعل الكود أكثر نظافة ويقلل من التعقيد الناتج عن الـ props drilling. لكن لا تخطئ: Context ليس بديلاً لمكتبات إدارة الحالة المتقدمة مثل Redux، بل هو أداة بسيطة لمشاركة البيانات على مستوى التطبيق دون الحاجة إلى مكتبات خارجية.
من الناحية الفنية، Context يعمل عن طريق إنشاء كائن context يحتوي على Provider و Consumer. الـ Provider هو مكون يوفر القيمة الحالية للـ context لجميع المكونات الأبناء، بينما الـ Consumer هو مكون يستخدم هذه القيمة. عندما تتغير القيمة المقدمة من الـ Provider، فإن جميع المكونات التي تستخدم هذه القيمة ستعيد الـ render تلقائياً. هذا يعني أن استخدام Context بشكل غير صحيح يمكن أن يؤدي إلى مشاكل في الأداء، حيث قد يؤدي تحديث بسيط في القيمة إلى إعادة render لجميع المكونات التي تستخدم هذا Context، حتى لو لم تكن بحاجة إلى هذا التحديث.
// مثال على استخدام Context API لمشاركة الحالة
import React, { createContext, useContext, useState } from 'react';
// إنشاء Context
const ThemeC createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
onClick={toggleTheme}
style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#000' : '#fff',
}}
>
Toggle Theme
</button>
);
}
function App() {
return (
<ThemeProvider>
<div>
<h1>Theme Switcher</h1>
<ThemedButton />
</div>
</ThemeProvider>
);
}Context API هو الحل الأمثل عندما تحتاج لمشاركة بيانات بسيطة بين مكونات متعددة دون الحاجة إلى مكتبة خارجية. مثلاً، إذا كنت بحاجة لمشاركة حالة الثيم أو بيانات المستخدم أو إعدادات اللغة بين عدة مكونات، فإن Context يمكن أن يكون خياراً جيداً. أيضاً، إذا كنت تعمل على مشروع صغير أو متوسط الحجم ولا تريد إضافة تبعيات خارجية، فإن Context يمكن أن يكون حلاً كافياً لإدارة الحالة المشتركة.
لكن Context ليس مناسباً لكل الحالات. إذا كانت البيانات التي تريد مشاركتها تتغير بشكل متكرر، فإن استخدام Context قد يؤدي إلى مشاكل في الأداء، حيث سيؤدي كل تحديث إلى إعادة render لجميع المكونات التي تستخدم هذا Context. أيضاً، إذا كنت بحاجة إلى منطق تحديث معقد أو ميزات متقدمة مثل الـ middleware أو الـ time-travel debugging، فإن Context وحده لن يكون كافياً، وستحتاج إلى استخدام مكتبة خارجية مثل Redux أو Zustand. في تجربتي، أفضل استخدام Context عندما تكون البيانات التي أريد مشاركتها ثابتة نسبياً أو تتغير بشكل نادر، مثل حالة الثيم أو بيانات المستخدم.
عندما نتحدث عن إدارة الحالة في React، فإن Redux هو أول مكتبة تخطر على البال. منذ إطلاقها في ٢٠١٥، أصبحت Redux المعيار الذهبي لإدارة الحالة في التطبيقات الكبيرة، وقد تم تبنيها من قبل شركات مثل Twitter وNetflix وUber. لكن مع ظهور مكتبات أحدث مثل Zustand وJotai، بدأ الكثير من المطورين يتساءلون: هل لا يزال Redux له مكان في ٢٠٢٤؟ الحقيقة هي أن Redux لا يزال أداة قوية لإدارة الحالة في التطبيقات الكبيرة والمعقدة، لكنها ليست الحل الأمثل لكل مشروع.
من الناحية الفنية، Redux يعتمد على ثلاثة مبادئ أساسية: الـ single source of truth، حيث يتم تخزين الحالة بأكملها في كائن واحد، والـ state is read-only، حيث لا يمكن تعديل الحالة مباشرة بل يجب إرسال إجراءات (actions) لتحديثها، والـ changes are made with pure functions، حيث يتم استخدام الـ reducers لتحديث الحالة بناءً على الإجراءات. هذا النمط يجعل من السهل تتبع التغيرات في الحالة وفهم كيفية تطور البيانات مع مرور الوقت، كما يجعل من السهل إضافة ميزات متقدمة مثل الـ middleware والـ time-travel debugging.
// مثال على استخدام Redux لإدارة الحالة
import { createStore } from 'redux';
import { Provider, useSelector, useDispatch } from 'react-redux';
// تعريف الـ actions
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const DELETE_TODO = 'DELETE_TODO';
// تعريف الـ action creators
const addTodo = (text) => ({ type: ADD_TODO, text });
const toggleTodo = (index) => ({ type: TOGGLE_TODO, index });
const deleteTodo = (index) => ({ type: DELETE_TODO, index });
// تعريف الـ reducer
const todoReducer = (state = { todos: [] }, action) => {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [...state.todos, { text: action.text, completed: false }],
};
case TOGGLE_TODO:
return {
...state,
todos: state.todos.map((todo, index) =>
index === action.index ? { ...todo, completed: !todo.completed } : todo
),
};
case DELETE_TODO:
return {
...state,
todos: state.todos.filter((_, index) => index !== action.index),
};
default:
return state;
}
};
// إنشاء الـ store
const store = createStore(todoReducer);
function TodoApp() {
const todos = useSelector((state) => state.todos);
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
const input = e.target.elements.todoInput;
dispatch(addTodo(input.value));
input.value = '';
};
return (
<div>
<form {handleSubmit}>
<input name="todoInput" placeholder="Add a new task" />
<button type="submit">Add</button>
</form>
<ul>
{todos.map((todo, index) => (
<li key={index}>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => dispatch(toggleTodo(index))}>
Toggle
</button>
<button onClick={() => dispatch(deleteTodo(index))}>
Delete
</button>
</li>
))}
</ul>
</div>
);
}
function App() {
return (
<Provider store={store}>
<TodoApp />
</Provider>
);
}Redux هو الخيار الأمثل عندما تحتاج إلى إدارة حالة معقدة في تطبيق كبير يتطلب ميزات متقدمة مثل الـ middleware، والـ time-travel debugging، والـ undo/redo. مثلاً، إذا كنت تبني لوحة تحكم إدارية تحتوي على عدة أقسام مترابطة، وتحتاج إلى تتبع كل تغيير في الحالة، فإن Redux يمكن أن يكون الحل الأمثل. أيضاً، إذا كنت تعمل في فريق كبير وتحتاج إلى نمط موحد لإدارة الحالة، فإن Redux يوفر بنية واضحة تجعل من السهل على المطورين الجدد فهم كيفية تدفق البيانات في التطبيق.
لكن Redux ليس مناسباً لكل المشاريع. إذا كنت تعمل على تطبيق صغير أو متوسط الحجم، فإن استخدام Redux قد يكون مبالغة غير ضرورية ويضيف تعقيداً لا داعي له. أيضاً، إذا كنت بحاجة إلى أداء عالي جداً، فإن Redux قد يكون أبطأ من البدائل الحديثة مثل Zustand، حيث يعتمد على نمط الـ immutable updates والذي يمكن أن يؤدي إلى إعادة إنشاء كائنات جديدة في كل تحديث. في تجربتي، أفضل استخدام Redux عندما يكون التطبيق كبيراً ومعقداً، وتحتاج إلى ميزات متقدمة مثل الـ middleware أو عندما تعمل في فريق كبير وتحتاج إلى نمط موحد لإدارة الحالة.
إذا كنت تبحث عن بديل حديث لـ Redux يجمع بين البساطة والأداء العالي، فإن Zustand هو الخيار الأمثل. Zustand هي مكتبة إدارة حالة صغيرة الحجم وسريعة جداً، تعتمد على مفهوم الـ store المشابه لـ Redux، لكنها تتجنب التعقيد الزائد وتوفر واجهة برمجة بسيطة وسهلة الاستخدام. في عام ٢٠٢٣، أصبحت Zustand الخيار المفضل للعديد من المطورين الذين يريدون بديلاً حديثاً لـ Redux دون التضحية بالأداء أو الميزات المتقدمة.
من الناحية الفنية، Zustand يعتمد على مفهوم الـ store المشابه لـ Redux، لكنه يستخدم الـ proxies في JavaScript لمراقبة التغيرات في الحالة، مما يجعله أسرع بكثير من Redux في معظم الحالات. أيضاً، Zustand يدعم الـ middleware والـ devtools، مما يجعله مناسباً للتطبيقات الكبيرة والمعقدة. الفرق الرئيسي بين Zustand وRedux هو أن Zustand لا يفرض عليك استخدام الـ actions والـ reducers، بل يمكنك تعديل الحالة مباشرة داخل الـ store، مما يجعل الكود أكثر بساطة ومرونة.
// مثال على استخدام Zustand لإدارة الحالة
import create from 'zustand';
// إنشاء الـ store
const useStore = create((set) => ({
todos: [],
addTodo: (text) => set((state) => ({
todos: [...state.todos, { text, completed: false }],
})),
toggleTodo: (index) => set((state) => ({
todos: state.todos.map((todo, i) =>
i === index ? { ...todo, completed: !todo.completed } : todo
),
})),
deleteTodo: (index) => set((state) => ({
todos: state.todos.filter((_, i) => i !== index),
})),
}));
function TodoApp() {
const { todos, addTodo, toggleTodo, deleteTodo } = useStore();
const handleSubmit = (e) => {
e.preventDefault();
const input = e.target.elements.todoInput;
addTodo(input.value);
input.value = '';
};
return (
<div>
<form {handleSubmit}>
<input name="todoInput" placeholder="Add a new task" />
<button type="submit">Add</button>
</form>
<ul>
{todos.map((todo, index) => (
<li key={index}>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => toggleTodo(index)}>
Toggle
</button>
<button onClick={() => deleteTodo(index)}>
Delete
</button>
</li>
))}
</ul>
</div>
);
}Zustand هو الخيار الأمثل عندما تريد بديلاً حديثاً لـ Redux يجمع بين البساطة والأداء العالي. مثلاً، إذا كنت تعمل على تطبيق متوسط أو كبير الحجم وتحتاج إلى إدارة حالة معقدة دون التعقيد الزائد لـ Redux، فإن Zustand يمكن أن يكون الحل الأمثل. أيضاً، إذا كنت بحاجة إلى أداء عالي جداً، فإن Zustand أسرع بكثير من Redux في معظم الحالات، حيث يعتمد على الـ proxies لمراقبة التغيرات بدلاً من الاعتماد على نمط الـ immutable updates.
لكن Zustand ليس مناسباً لكل الحالات. إذا كنت بحاجة إلى ميزات متقدمة مثل الـ time-travel debugging أو إذا كنت تعمل في فريق كبير وتحتاج إلى نمط موحد لإدارة الحالة، فإن Redux قد يكون الخيار الأفضل. أيضاً، إذا كنت تعمل على مشروع صغير ولا تحتاج إلى مكتبة خارجية، فإن استخدام useState مع Context API قد يكون كافياً. في تجربتي، أفضل استخدام Zustand عندما أريد بديلاً حديثاً لـ Redux يجمع بين البساطة والأداء العالي، وعندما لا أحتاج إلى ميزات متقدمة مثل الـ time-travel debugging.
بعد استعراض كل هذه الأدوات، قد تشعر بالحيرة: أي أداة تختار لمشروعك؟ الحقيقة هي أنه لا يوجد حل واحد يناسب الجميع، والاختيار يعتمد على عدة عوامل مثل حجم المشروع، وتعقيد الحالة، واحتياجات الفريق. لكن هناك قاعدة ذهبية يمكن أن تساعدك في اتخاذ القرار: ابدأ بالحل الأبسط الذي يلبي احتياجاتك، ثم انتقل إلى حلول أكثر تعقيداً فقط عندما تصبح الحاجة ملحة.
إذا كنت تعمل على مشروع صغير أو متوسط الحجم وتحتاج إلى إدارة حالة بسيطة، فابدأ بـ useState وuseReducer. إذا كنت بحاجة لمشاركة الحالة بين مكونات متعددة دون تعقيد، فاستخدم Context API. إذا كنت تعمل على تطبيق كبير ومعقد وتحتاج إلى ميزات متقدمة مثل الـ middleware والـ time-travel debugging، فاستخدم Redux. وإذا كنت تريد بديلاً حديثاً لـ Redux يجمع بين البساطة والأداء العالي، فاختر Zustand. في النهاية، أهم شيء هو فهم احتياجات مشروعك واختيار الأداة التي تلبي هذه الاحتياجات دون إضافة تعقيد غير ضروري.
وأخيراً، تذكر أن إدارة الحالة في React ليست مجرد اختيار أداة، بل هي قرار هندسي يؤثر على كل جانب من جوانب تطبيقك. اختر بحكمة، وابحث دائماً عن التوازن بين البساطة والأداء، ولا تقع في فخ التعقيد الزائد أو النقص المؤلم. في عالم البرمجة، كما في الحياة، البساطة غالباً ما تكون الحل الأمثل.