هل تستخدم useState لكل شيء؟ أم أنك غرقت في Redux دون داع؟ هذا الدليل العملي يفكك لك متى تستخدم كل أداة لإدارة الحالة في React، مع شرح تقني عميق لما يحدث خلف الكواليس في الذاكرة والمعالج.
في أحد المشاريع الكبيرة التي عملت عليها مع فريق مكون من 12 مطوراً، كان لدينا مكون واحد فقط مسؤول عن فلترة البيانات في لوحة التحكم الرئيسية. هذا المكون كان يستخدم 7 حالات مختلفة عبر useState، وكان يعيد الرسم 14 مرة في الثانية عند تغيير أي فلتر. النتيجة؟ التطبيق أصبح أبطأ من سلحفاة في سباق ماراثون، والـ Event Loop كان يغلي من كثرة الـ re-renders. المشكلة لم تكن في React نفسه، بل في أننا اخترنا الأداة الخطأ لإدارة الحالة. في هذا المقال، سأريك بالضبط متى تستخدم useState، متى تنتقل لـ useReducer، ومتى يجب أن تفكر في حلول خارجية مثل Zustand أو Redux، مع شرح تقني لما يحدث خلف الكواليس في كل خيار.
الخطأ الشائع الذي أراه في معظم المشاريع هو أن المطورين يختارون أداة إدارة الحالة بناءً على الشعبية أو التعقيد، وليس بناءً على الاحتياجات الفعلية للتطبيق. مثلاً، Redux أصبح مثل السكين السويسري الذي يستخدمونه حتى لفتح علبة مشروبات غازية. الحقيقة هي أن كل أداة لها سيناريو محدد تعمل فيه بكفاءة عالية، وخارج هذا السيناريو تصبح عبئاً على الأداء والمطورين على حد سواء. دعونا نبدأ بتشريح كل أداة على حدة، ونرى بالضبط ماذا يحدث في الذاكرة والمعالج عندما تستخدمها.
useState هو أول أداة يتعلمها المطورون في React، وغالباً ما يكون آخر أداة يفكرون فيها عند إدارة الحالة. هذا خطأ كبير. useState مصمم للحالات البسيطة والمعزولة التي لا تحتاج إلى منطق معقد أو مشاركة بين مكونات بعيدة. عندما تستدعي useState، React يقوم بإنشاء متغير في ذاكرة المكون (Component Memory) ويربطه بوظيفة تحديث. عند استدعاء وظيفة التحديث، React يقوم بتشغيل عملية الـ reconciliation، حيث يقارن الـ Virtual DOM القديم بالجديد ويحدد بالضبط ما يحتاج إلى تحديث في الـ Real DOM.
المشكلة تبدأ عندما تستخدم useState لإدارة حالات معقدة أو مترابطة. مثلاً، إذا كان لديك مكون يحتوي على 5 حالات مختلفة وكل حالة تعتمد على الأخرى، فإن تغيير واحدة منها سيؤدي إلى إعادة رسم المكون بالكامل، حتى لو لم تتأثر بعض الأجزاء. هذا لأن React لا يعرف أن بعض الحالات غير مرتبطة ببعضها، فيعيد رسم كل شيء. في مشروع سابق، كان لدينا مكون فلترة يحتوي على حالات مثل: selectedCategory، priceRange، isFeaturedOnly، sortBy، وsearchQuery. عند تغيير أي منها، كان المكون يعيد الرسم بالكامل، مما أدى إلى بطء ملحوظ عند استخدام الـ debounce مع مدخلات المستخدم.
// مثال سيء: استخدام useState لحالات مترابطة
import React, { useState } from 'react';
function ProductFilter() {
const [selectedCategory, setSelectedCategory] = useState('all');
const [priceRange, setPriceRange] = useState([0, 1000]);
const [isFeaturedOnly, setIsFeaturedOnly] = useState(false);
const [sortBy, setSortBy] = useState('price');
const [searchQuery, setSearchQuery] = useState('');
// عند تغيير أي حالة، المكون يعيد الرسم بالكامل
const handleCategoryChange = (category) => {
setSelectedCategory(category);
// هنا React يعيد رسم المكون بالكامل
};
return (
<div>
{/* عناصر الفلترة هنا */}
</div>
);
}الحل الأمثل في هذه الحالة هو إما تقسيم المكون إلى مكونات أصغر، أو استخدام أداة أكثر ملاءمة مثل useReducer. لكن متى يجب أن تبقى مع useState؟ عندما تكون الحالة بسيطة ومعزولة ولا تحتاج إلى منطق معقد للتحديث. مثلاً، حالة زر تبديل، أو مدخل نصي بسيط، أو حالة تحميل. في هذه الحالات، useState هو الخيار الأمثل لأنه خفيف الوزن وسهل الاستخدام.
useReducer هو الخطوة المنطقية التالية عندما تجد نفسك تكتب الكثير من useState مع منطق تحديث معقد. فكر في useReducer كآلة حالة (State Machine) داخل مكونك. بدلاً من كتابة عدة useState وحالات متفرقة، تجمع كل الحالات في كائن واحد، وكل منطق التحديث في دالة reducer واحدة. هذا النهج له عدة مزايا: أولاً، يجعل منطق التحديث مركزاً وسهل الصيانة. ثانياً، يقلل من عدد مرات إعادة الرسم لأنك تستطيع التحكم في متى وكيف تحدث التحديثات. ثالثاً، يجعل من السهل تتبع تدفق البيانات داخل المكون.
عندما تستدعي useReducer، React يقوم بإنشاء كائن حالة مركزي في ذاكرة المكون، ويربطه بدالة reducer مسؤولة عن جميع التحديثات. عند استدعاء dispatch، React يمرر الإجراء (action) إلى دالة reducer، التي تقوم بتحديث الحالة بناءً على نوع الإجراء. الفرق الرئيسي عن useState هو أن useReducer يسمح لك بفصل منطق التحديث عن واجهة المستخدم، مما يجعل الكود أكثر قابلية للاختبار والصيانة.
// مثال جيد: استخدام useReducer لحالات مترابطة
import React, { useReducer } from 'react';
const initialState = {
selectedCategory: 'all',
priceRange: [0, 1000],
isFeaturedOnly: false,
sortBy: 'price',
searchQuery: '',
};
function filterReducer(state, action) {
switch (action.type) {
case 'SET_CATEGORY':
return { ...state, selectedCategory: action.payload };
case 'SET_PRICE_RANGE':
return { ...state, priceRange: action.payload };
case 'TOGGLE_FEATURED':
return { ...state, isFeaturedOnly: !state.isFeaturedOnly };
case 'SET_SORT_BY':
return { ...state, sortBy: action.payload };
case 'SET_SEARCH_QUERY':
return { ...state, searchQuery: action.payload };
case 'RESET':
return initialState;
default:
return state;
}
}
function ProductFilter() {
const [state, dispatch] = useReducer(filterReducer, initialState);
const handleCategoryChange = (category) => {
dispatch({ type: 'SET_CATEGORY', payload: category });
// هنا React يعيد رسم المكون مرة واحدة فقط
};
return (
<div>
{/* عناصر الفلترة هنا باستخدام state */}
</div>
);
}في المثال أعلاه، لاحظ كيف أن كل تغيير في الحالة يتم عبر dispatch، مما يجعل منطق التحديث مركزاً وسهل التعديل. أيضاً، لأن كل الحالات مجمعة في كائن واحد، يمكنك بسهولة إعادة تعيين كل الفلاتر إلى القيم الافتراضية باستخدام إجراء واحد (RESET). هذا النهج يقلل بشكل كبير من عدد مرات إعادة الرسم، خاصة إذا كنت تستخدم React.memo للمكونات الفرعية.
لكن useReducer ليس حلاً سحرياً. إذا كانت حالاتك تحتاج إلى مشاركة بين مكونات بعيدة في شجرة المكونات، أو إذا كان لديك منطق تحديث معقد جداً يتطلب middleware مثل الـ thunk أو الـ saga، فقد حان الوقت للتفكير في حلول خارجية. أيضاً، إذا كنت تجد نفسك تكتب الكثير من useReducer متشابهة في مكونات مختلفة، فهذا مؤشر على أنك بحاجة إلى حل مركزي لإدارة الحالة.
Zustand هو المكتبة التي أحب استخدامها في المشاريع المتوسطة الحجم. إنها خفيفة الوزن (أقل من 1 كيلوبايت)، وسهلة الإعداد، وتوفر طريقة بسيطة لإدارة الحالة المشتركة بين المكونات دون الحاجة إلى boilerplate معقد مثل Redux. الفكرة الأساسية وراء Zustand هي إنشاء متجر مركزي (store) يمكن لأي مكون الوصول إليه وتحديثه دون الحاجة إلى تمرير الحالة عبر الـ props بشكل متكرر.
عندما تنشئ متجر Zustand، المكتبة تقوم بإنشاء كائن حالة مركزي في ذاكرة التطبيق (ليس في ذاكرة المكون). هذا المتجر يمكن الوصول إليه من أي مكون عبر hook بسيط. عند تحديث الحالة في المتجر، Zustand يستخدم آلية تسمى