هل تشعر بالحيرة بين useState وuseReducer وContext وRedux وZustand؟ هذا الدليل العملي يكشف متى تستخدم كل أداة، وكيف تعمل خلف الكواليس، وما هي الفخاخ الحقيقية التي يقع فيها المطورون في الإنتاج.
في آخر مشروع عملت عليه، كان لدينا مكون واحد فقط مسؤول عن إدارة طلبات الدفع في منصة مالية. بعد ثلاثة أشهر، تحول هذا المكون إلى وحش من ٨٠٠ سطر، فيه ١٢ useState و٥ useEffect متداخلة، وكل تعديل كان يكسر شيئاً آخر. المشكلة لم تكن في الكود نفسه، بل في أننا اخترنا الأداة الخطأ لإدارة الحالة منذ البداية. useState كان مناسباً في البداية، لكن عندما بدأت الحالة تتعقد وتتداخل، كان يجب علينا الانتقال إلى شيء أكثر قوة. هذا المقال ليس مجرد شرح للأدوات، بل هو خريطة طريق عملية تخبرك بالضبط متى تستخدم ماذا، وكيف تفكر كالمهندس الذي يبني تطبيقات قابلة للصيانة على المدى الطويل.
سنتجاوز التعريفات الجافة ونذهب مباشرة إلى قلب المشكلة: كيف تخزن React الحالة في الذاكرة؟ وكيف تؤثر اختياراتك على أداء التطبيق؟ سنرى مثلاً كيف أن useState يُعيد بناء المكون بالكامل عند تغيير القيمة، بينما useReducer يسمح بتحديثات أكثر دقة. وسنتحدث عن Context وكيف أنه ليس حلاً سحرياً لإدارة الحالة، بل أداة لنقل البيانات عبر الشجرة دون props drilling. وأخيراً، سنقارن بين Redux وZustand ونرى لماذا تفضل شركات مثل Airbnb وNetflix Zustand في مشاريعها الحديثة.
useState هو أول أداة تتعلمها في React، وغالباً ما يكون أول أداة تستخدمها لإدارة الحالة. وهو مثالي للحالات البسيطة مثل حقول الإدخال، أو الأزرار المتغيرة، أو أي حالة محلية داخل مكون واحد. لكن المشكلة تبدأ عندما تبدأ الحالة في النمو. مثلاً، إذا كان لديك مكون لإدارة نموذج تسجيل المستخدم، قد تبدأ بـ useState بسيط:
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: ''
});
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};هذا الكود يبدو بريئاً، لكنه يخفي مشكلة كبيرة: عند كل تغيير في الحقل، React تُعيد بناء الكائن بالكامل في الذاكرة، ثم تُعيد بناء المكون. في التطبيقات الصغيرة، هذا ليس مشكلة، لكن في التطبيقات الكبيرة، هذا يمكن أن يؤدي إلى إعادة بناء غير ضرورية للمكونات، مما يؤثر على الأداء. بالإضافة إلى ذلك، إذا كان لديك منطق معقد لتحديث الحالة، مثل التحقق من تطابق كلمات المرور، أو إرسال طلب API عند تغيير الحقل، فإن useState يصبح غير مناسب. هنا يأتي دور useReducer كبديل أقوى وأكثر تنظيماً.
في أحد المشاريع السابقة، استخدمنا useState لإدارة حالة فلتر معقد في لوحة تحكم إدارية. كان لدينا أكثر من ١٠ حقول فلتر، وكل حقل كان يؤثر على الحقول الأخرى. بعد فترة، أصبح الكود غير قابل للصيانة، وكنا نضطر إلى كتابة useEffect معقدة للتأكد من أن التحديثات تحدث بالترتيب الصحيح. الحل كان الانتقال إلى useReducer، الذي وفر لنا طريقة منظمة لتحديث الحالة بناءً على الأحداث.
useReducer هو الخطوة الطبيعية التالية عندما يصبح useState غير كافٍ. إنه يوفر طريقة منظمة لتحديث الحالة بناءً على الأحداث، ويشبه إلى حد كبير كيفية عمل Redux، لكنه مدمج داخل React. الفكرة الأساسية هي أن لديك حالة أولية، ودالة reducer تأخذ الحالة الحالية وإجراءً (action)، ثم تُرجع الحالة الجديدة. هذا النمط مفيد جداً عندما يكون لديك منطق تحديث معقد، أو عندما تعتمد التحديثات على الحالة الحالية.
const initialState = {
name: '',
email: '',
password: '',
confirmPassword: '',
isSubmitting: false,
error: null
};
function formReducer(state, action) {
switch (action.type) {
case 'FIELD_CHANGE':
return {
...state,
[action.field]: action.value
};
case 'SUBMIT_START':
return {
...state,
isSubmitting: true,
error: null
};
case 'SUBMIT_SUCCESS':
return initialState;
case 'SUBMIT_FAILURE':
return {
...state,
isSubmitting: false,
error: action.error
};
default:
return state;
}
}
function RegistrationForm() {
const [state, dispatch] = useReducer(formReducer, initialState);
const handleChange = (e) => {
dispatch({
type: 'FIELD_CHANGE',
field: e.target.name,
value: e.target.value
});
};
const handleSubmit = async (e) => {
e.preventDefault();
dispatch({ type: 'SUBMIT_START' });
try {
await api.submitForm(state);
dispatch({ type: 'SUBMIT_SUCCESS' });
} catch (error) {
dispatch({ type: 'SUBMIT_FAILURE', error: error.message });
}
};
return (
<form {handleSubmit}>
<input name="name" value={state.name} onChange={handleChange} />
<input name="email" value={state.email} onChange={handleChange} />
<input name="password" type="password" value={state.password} onChange={handleChange} />
<input name="confirmPassword" type="password" value={state.confirmPassword} onChange={handleChange} />
<button type="submit" disabled={state.isSubmitting}>Submit</button>
{state.error && <p>{state.error}</p>}
</form>
);
}هذا الكود أكثر تنظيماً بكثير من استخدام useState المتعدد. لاحظ كيف أن كل تحديث للحالة يتم عبر dispatch، وهذا يجعل من السهل تتبع ما يحدث في التطبيق. بالإضافة إلى ذلك، useReducer يسمح لك بتجميع كل منطق التحديث في مكان واحد، مما يجعل الكود أسهل في الصيانة والتعديل. لكن حتى useReducer له حدوده. إذا كنت بحاجة إلى مشاركة الحالة بين مكونات متعددة، أو إذا كانت الحالة تحتاج إلى أن تكون متاحة عبر التطبيق بأكمله، فإن Context يصبح ضرورياً.
في أحد المشاريع، استخدمنا useReducer لإدارة حالة سلة التسوق في متجر إلكتروني. كان لدينا منطق معقد لتحديث الكميات، وحسابات الخصومات، وتحديث الأسعار بناءً على الموقع الجغرافي. useReducer جعل من السهل إدارة كل هذا المنطق في مكان واحد، وكان من السهل إضافة ميزات جديدة دون كسر الكود الحالي.
كثير من المطورين يعتقدون أن Context هو حل لإدارة الحالة، لكنه في الحقيقة ليس كذلك. Context هو أداة لنقل البيانات عبر شجرة المكونات دون الحاجة إلى تمرير props يدوياً عبر كل مستوى. إنه مفيد جداً عندما تحتاج إلى مشاركة بيانات مثل اللغة الحالية، أو السمة اللونية، أو حالة المصادقة بين مكونات متعددة. لكن استخدام Context لإدارة الحالة المعقدة يمكن أن يؤدي إلى مشاكل في الأداء، لأن أي تغيير في قيمة Context يؤدي إلى إعادة بناء جميع المكونات التي تستهلك هذا Context، حتى لو لم تكن بحاجة إلى إعادة البناء.
// ThemeContext.js
import { createContext, useContext, useState } from 'react';
const ThemeC createContext();
export 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>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
// App.js
import { ThemeProvider } from './ThemeContext';
function App() {
return (
<ThemeProvider>
<Toolbar />
</ThemeProvider>
);
}
// Toolbar.js
import { useTheme } from './ThemeContext';
function Toolbar() {
const { theme, toggleTheme } = useTheme();
return (
<div style={{ background: theme === 'light' ? '#fff' : '#333' }}>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
}في هذا المثال، Context يُستخدم لمشاركة حالة السمة اللونية بين مكونات متعددة. هذا الاستخدام مناسب جداً، لأن السمة اللونية هي حالة بسيطة لا تتغير كثيراً، والمكونات التي تستهلكها تحتاج إلى إعادة البناء عند تغييرها. لكن إذا استخدمت Context لإدارة حالة معقدة مثل سلة التسوق، فإن أي تغيير في السلة سيؤدي إلى إعادة بناء جميع المكونات التي تستهلك هذا Context، مما يؤثر على الأداء. لهذا السبب، Context ليس حلاً لإدارة الحالة المعقدة، بل أداة لنقل البيانات عبر الشجرة.
في أحد المشاريع، استخدمنا Context لإدارة حالة المصادقة في تطبيق كبير. كان لدينا مكونات متعددة تحتاج إلى معرفة ما إذا كان المستخدم مسجلاً دخول أم لا، وما هي صلاحياته. Context كان حلاً مناسباً هنا، لأنه كان تغييراً نادراً (عند تسجيل الدخول أو الخروج)، ولم نكن بحاجة إلى إعادة بناء المكونات عند كل تغيير بسيط في الحالة. لكن عندما حاولنا استخدام Context لإدارة حالة الفلتر في لوحة تحكم معقدة، واجهنا مشاكل في الأداء، لأن أي تغيير في الفلتر كان يؤدي إلى إعادة بناء جميع المكونات التي تستهلك هذا Context.
Redux كان الحل السائد لإدارة الحالة في تطبيقات React لسنوات طويلة. إنه يوفر طريقة مركزية لإدارة الحالة، مع store واحد يحتوي على كل حالة التطبيق، وreducers لتحديث هذه الحالة بناءً على actions. لكن مع ظهور Hooks، أصبح Redux أقل جاذبية للكثير من المطورين. السبب الرئيسي هو التعقيد: تحتاج إلى كتابة الكثير من الكود لإنشاء store، وactions، وreducers، ومكونات متصلة. بالإضافة إلى ذلك، Redux يعتمد على نمط التحديث غير المباشر، حيث ترسل actions إلى store، ثم reducers تعالج هذه Actions وتحدث الحالة. هذا النمط يمكن أن يكون مفيداً في التطبيقات الكبيرة، لكنه يضيف تعقيداً غير ضروري في التطبيقات الصغيرة والمتوسطة.
// store.js
import { createStore } from 'redux';
const initialState = {
counter: 0
};
function counterReducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, counter: state.counter + 1 };
case 'DECREMENT':
return { ...state, counter: state.counter - 1 };
default:
return state;
}
}
const store = createStore(counterReducer);
export default store;
// Counter.js
import { useSelector, useDispatch } from 'react-redux';
function Counter() {
const counter = useSelector(state => state.counter);
const dispatch = useDispatch();
return (
<div>
<button {() => dispatch({ type: 'DECREMENT' })}>-</button>
<span>{counter}</span>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
</div>
);
}هذا المثال يظهر مدى تعقيد Redux حتى لمثال بسيط مثل عداد. تحتاج إلى إنشاء store، وكتابة reducer، واستخدام useSelector وuseDispatch في المكونات. هذا التعقيد يمكن أن يكون مبرراً في التطبيقات الكبيرة التي تحتاج إلى إدارة حالة معقدة، أو عندما تحتاج إلى ميزات متقدمة مثل middleware للتحكم في تدفق البيانات. لكن في التطبيقات الصغيرة والمتوسطة، هذا التعقيد غير ضروري، ويمكن استبداله بأدوات أبسط مثل Zustand.
في أحد المشاريع الكبيرة الذي عملت عليه، استخدمنا Redux لإدارة حالة معقدة تتضمن بيانات المستخدم، والطلبات، والإعدادات، والتفضيلات. كان لدينا أكثر من ٢٠ reducer، وmiddleware مخصصة للتعامل مع الطلبات غير المتزامنة. Redux كان حلاً مناسباً هنا، لأنه وفر لنا طريقة منظمة لإدارة كل هذه الحالة المعقدة. لكن في مشروع آخر أصغر، حاولنا استخدام Redux لإدارة حالة بسيطة، ووجدنا أنفسنا نكتب الكثير من الكود غير الضروري، وكان من الأسهل بكثير استخدام Zustand أو حتى useReducer مع Context.
Zustand هو مكتبة إدارة حالة حديثة أصبحت شائعة جداً في السنوات الأخيرة. السبب الرئيسي لشعبيتها هو البساطة: فهي توفر طريقة مباشرة لإدارة الحالة دون الحاجة إلى كتابة الكثير من الكود. مع Zustand، يمكنك إنشاء store يحتوي على الحالة والمنطق لتحديثها، ثم استخدام هذا Store في أي مكون دون الحاجة إلى توصيله بشكل صريح. هذا يجعل Zustand حلاً مثالياً للتطبيقات المتوسطة والكبيرة التي تحتاج إلى إدارة حالة معقدة دون التعقيد الذي يأتي مع Redux.
// store.js
import create from 'zustand';
const useStore = create(set => ({
counter: 0,
increment: () => set(state => ({ counter: state.counter + 1 })),
decrement: () => set(state => ({ counter: state.counter - 1 }))
}));
// Counter.js
export default function Counter() {
const { counter, increment, decrement } = useStore();
return (
<div>
<button {decrement}>-</button>
<span>{counter}</span>
<button onClick={increment}>+</button>
</div>
);
}هذا المثال يظهر مدى بساطة Zustand. يمكنك إنشاء store يحتوي على الحالة والمنطق لتحديثها في مكان واحد، ثم استخدام هذا Store في أي مكون دون الحاجة إلى توصيله بشكل صريح. بالإضافة إلى ذلك، Zustand يدعم ميزات متقدمة مثل middleware، وtime-travel debugging، والتحديثات غير المتزامنة، لكنه يفعل ذلك بطريقة أبسط بكثير من Redux. وهذا هو السبب في أن الكثير من الشركات مثل Airbnb وNetflix بدأت في استخدام Zustand في مشاريعها الحديثة.
في آخر مشروع عملت عليه، استخدمنا Zustand لإدارة حالة معقدة تتضمن بيانات المستخدم، والطلبات، والإعدادات، والتفضيلات. كان لدينا store واحد يحتوي على كل هذه الحالة، وكان من السهل جداً إضافة ميزات جديدة وتحديث الحالة دون الحاجة إلى كتابة الكثير من الكود. بالإضافة إلى ذلك، Zustand كان أسرع بكثير من Redux في إعادة البناء، لأن Zustand يستخدم آلية تحديث أكثر دقة، مما يعني أن المكونات لا تُعاد بناؤها إلا عند الحاجة.
عندما تختار أداة لإدارة الحالة، يجب أن تفكر في كيفية تأثيرها على أداء التطبيق. مثلاً، useState يُعيد بناء المكون بالكامل عند تغيير القيمة، بينما useReducer يسمح بتحديثات أكثر دقة. Context يُعيد بناء جميع المكونات التي تستهلكه عند أي تغيير، حتى لو لم تكن بحاجة إلى إعادة البناء. Redux يُعيد بناء المكونات المتصلة عند تغيير الحالة، لكن فقط إذا كانت هذه المكونات تستهلك الجزء المتغير من الحالة. Zustand يُعيد بناء المكونات فقط عندما تتغير القيمة التي تستهلكها، مما يجعله أكثر كفاءة من Redux في معظم الحالات.
لنأخذ مثالاً عملياً: إذا كان لديك مكون يعرض قائمة من العناصر، وكل عنصر يحتوي على زر لحذفه. إذا استخدمت useState لإدارة هذه القائمة، فإن أي تغيير في القائمة سيؤدي إلى إعادة بناء المكون بالكامل، بما في ذلك جميع العناصر. إذا استخدمت useReducer، يمكنك تحسين الأداء قليلاً عن طريق تحديث القائمة بطريقة أكثر دقة. إذا استخدمت Context، فإن أي تغيير في القائمة سيؤدي إلى إعادة بناء جميع المكونات التي تستهلك هذا Context، حتى لو كانت هذه المكونات لا تعرض القائمة. إذا استخدمت Redux أو Zustand، فإن المكونات ستُعاد بناؤها فقط إذا كانت تستهلك الجزء المتغير من الحالة، مما يجعلها أكثر كفاءة.
// مثال على أداء Zustand
import create from 'zustand';
const useStore = create(set => ({
items: [],
addItem: (item) => set(state => ({ items: [...state.items, item] })),
removeItem: (id) => set(state => ({ items: state.items.filter(item => item.id !== id) }))
}));
function ItemList() {
const { items, removeItem } = useStore();
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
<button {() => removeItem(item.id)}>Delete</button>
</li>
))}
</ul>
);
}في هذا المثال، Zustand يُعيد بناء مكون ItemList فقط عند تغيير قائمة العناصر، ولا يُعيد بناء المكونات الفردية عند حذف عنصر معين، لأن Zustand يستخدم آلية تحديث دقيقة. هذا يجعل Zustand أكثر كفاءة من Context وuseState في هذه الحالة. لكن إذا كانت القائمة صغيرة جداً، فإن الفرق في الأداء سيكون ضئيلاً، ويمكنك استخدام أي أداة دون قلق.
بعد كل هذه المعلومات، كيف تختار الأداة المناسبة لإدارة الحالة في مشروعك؟ إليك خريطة طريق عملية بناءً على حجم وتعقيد المشروع:
في رأيي الشخصي، Zustand هو الخيار الأفضل لمعظم المشاريع الحديثة. إنه يوفر توازناً رائعاً بين البساطة والقوة، ويجعل إدارة الحالة أسهل بكثير من Redux. بالإضافة إلى ذلك، Zustand أسرع وأكثر كفاءة من Redux في معظم الحالات. لكن إذا كنت تعمل على مشروع كبير جداً وتحتاج إلى ميزات متقدمة، فقد يكون Redux هو الخيار الأفضل. أما بالنسبة لـ useState وuseReducer وContext، فهي أدوات رائعة للحالات البسيطة والمتوسطة، ويجب استخدامها عندما تكون مناسبة.
القرار النهائي هو: لا تخف من تجربة الأدوات المختلفة واختيار ما يناسب مشروعك. إدارة الحالة ليست علم صارم، بل هي مهارة تتطور مع الخبرة. وكلما فهمت كيف تعمل هذه الأدوات خلف الكواليس، كلما أصبحت قراراتك أفضل وأكثر استنارة.
إذا كنت تريد نصيحة واحدة تغنيك عن قراءة هذا المقال بالكامل، فهي هذه: ابدأ دائماً بـ useState أو useReducer للحالات البسيطة، وانتقل إلى Zustand عندما تصبح الحالة معقدة وتحتاج إلى مشاركة بين مكونات متعددة. تجنب Redux إلا إذا كنت بحاجة إلى ميزات متقدمة جداً، وتجنب Context لإدارة الحالة المعقدة لأنه سيؤثر على الأداء. بهذه الطريقة، ستحصل على أفضل توازن بين البساطة والأداء في معظم المشاريع.