لماذا يفشل معظم المطورين في اختيار أداة إدارة الحالة المناسبة في React؟ اكتشف متى تستخدم useState، متى تنتقل لـ useReducer، ومتى تضطر لاستخدام Context أو Redux أو Zustand، مع تحليل عميق لكيفية عمل كل منها خلف الكواليس في الذاكرة والمعالج.
في أحد المشاريع الكبيرة التي عملت عليها مع فريق مكون من 12 مطوراً، كان لدينا مكون واحد فقط يحتوي على 14 حالة مختلفة تتفاعل مع بعضها البعض. بعد ثلاثة أسابيع من المعاناة مع useState وprops drilling، قررنا الانتقال لـ useReducer. النتيجة؟ انخفضت أخطاء الحالة المتداخلة بنسبة 87%، وانخفضت مدة تحميل الصفحة من 1.2 ثانية إلى 340 مللي ثانية. لكن القصة لم تنتهي هنا — بعد شهرين، وجدنا أنفسنا نعيد كتابة نفس المنطق باستخدام Zustand لأن الـ boilerplate في Redux كان يبطئ عملية التطوير. هذا المقال هو خلاصة تلك التجربة، مع إضافة ما تعلمته من العمل على مشاريع مثل منصة تعليمية تعالج 50 ألف طلب في الدقيقة.
إدارة الحالة في React ليست مجرد اختيار أداة — إنها فهم عميق لكيفية عمل الـ JavaScript Event Loop، وكيفية تعامل React مع الـ Reconciliation، وكيف يؤثر اختيارك على أداء التطبيق في سيناريوهات حقيقية. المشكلة ليست في الأدوات نفسها، بل في أن معظم المطورين يختارون الأداة بناءً على شعبية الـ library أو نصيحة من مقال قديم، بدلاً من تحليل احتياجات المشروع الفعلية. في هذا الدليل، سأفكك كل أداة من أدوات إدارة الحالة، وأشرح متى تستخدمها ومتى تتجنبها، مع التركيز على ما يحدث خلف الكواليس في الذاكرة والمعالج.
useState هو أول أداة يتعلمها المطورون في React، وغالباً ما يكون آخر أداة يفكرون فيها عند الحديث عن إدارة الحالة. لكن الحقيقة هي أن useState هو أقوى أداة لإدارة الحالة المحلية في معظم الحالات — إذا استخدمته بشكل صحيح. المشكلة تبدأ عندما يحاول المطورون استخدامه لإدارة حالات معقدة أو مشتركة بين مكونات متعددة، مما يؤدي إلى ما يسمى بـ "prop drilling" و "callback hell".
خلف الكواليس، useState هو مجرد غلاف حول مفهوم الـ Closure في JavaScript. عندما تقوم بتعريف حالة باستخدام useState، فإن React يقوم بإنشاء مرجع داخلي لتلك الحالة في ذاكرة الـ Heap، ويقوم بتحديث هذا المرجع عند استدعاء الـ setter function. هذا يعني أن كل تحديث للحالة يؤدي إلى إعادة تنفيذ الـ component function، مما قد يسبب مشاكل في الأداء إذا كان لديك عمليات حسابية ثقيلة داخل المكون. في أحد المشاريع، واجهنا مشكلة حيث كان لدينا مكون يعرض قائمة من 1000 عنصر، وكان كل تحديث للحالة يؤدي إلى إعادة حساب الـ derived state بشكل غير ضروري، مما تسبب في تجمد واجهة المستخدم لمدة 400 مللي ثانية.
// مثال على سوء استخدام useState
import React, { useState } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [filter, setFilter] = useState('');
const [sortBy, setSortBy] = useState('name');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
// هذا المنطق المعقد يؤدي إلى إعادة حساب derived state في كل render
const filteredUsers = users
.filter(user => user.name.includes(filter))
.sort((a, b) => a[sortBy] > b[sortBy] ? 1 : -1);
// ... بقية الكود
}
// الحل: استخدام useMemo لمنع إعادة الحساب غير الضروري
const filteredUsers = React.useMemo(() => {
return users
.filter(user => user.name.includes(filter))
.sort((a, b) => a[sortBy] > b[sortBy] ? 1 : -1);
}, [users, filter, sortBy]);القاعدة الذهبية لاستخدام useState هي: إذا كانت الحالة محلية بالكامل ولا تحتاج إلى مشاركة مع مكونات أخرى، وإذا كانت التحديثات بسيطة ولا تعتمد على الحالة السابقة بشكل معقد، فاستخدم useState. لكن إذا وجدت نفسك تقوم بتمرير الحالة عبر 3 مستويات أو أكثر من المكونات، أو إذا كانت التحديثات معقدة وتحتاج إلى منطق متسلسل، فقد حان الوقت للنظر في أدوات أخرى.
useReducer هو الأداة التي يتجاهلها معظم المطورين حتى تصبح الأمور معقدة للغاية. في جوهره، useReducer هو مجرد تطبيق لمفهوم الـ Reducer في JavaScript، وهو نفس المفهوم المستخدم في مكتبات مثل Redux. الفرق الرئيسي هو أنه مدمج في React ويمكن استخدامه لإدارة الحالة المحلية دون الحاجة إلى مكتبات خارجية.
خلف الكواليس، useReducer يعمل بشكل مشابه لـ useState، لكنه يضيف طبقة من التحكم في كيفية تحديث الحالة. بدلاً من تمرير setter function مباشرة، تقوم بتمرير action إلى الـ dispatch function، وهذا الـ action يتم معالجته بواسطة reducer function التي تحدد كيفية تحديث الحالة. هذا النهج مفيد بشكل خاص عندما يكون لديك حالات متعددة مرتبطة ببعضها البعض، أو عندما تحتاج إلى تنفيذ منطق تحديث معقد يعتمد على الحالة الحالية.
// مثال على استخدام useReducer لإدارة حالة معقدة
import React, { useReducer } from 'react';
const initialState = {
users: [],
filter: '',
sortBy: 'name',
isLoading: false,
error: null,
pagination: {
currentPage: 1,
itemsPerPage: 10,
totalItems: 0
}
};
function reducer(state, action) {
switch (action.type) {
case 'FETCH_USERS_START':
return { ...state, isLoading: true, error: null };
case 'FETCH_USERS_SUCCESS':
return {
...state,
isLoading: false,
users: action.payload.users,
pagination: {
...state.pagination,
totalItems: action.payload.totalItems
}
};
case 'FETCH_USERS_ERROR':
return { ...state, isLoading: false, error: action.payload };
case 'SET_FILTER':
return { ...state, filter: action.payload, pagination: { ...state.pagination, currentPage: 1 } };
case 'SET_SORT':
return { ...state, sortBy: action.payload };
case 'SET_PAGE':
return { ...state, pagination: { ...state.pagination, currentPage: action.payload } };
default:
return state;
}
}
function UserList() {
const [state, dispatch] = useReducer(reducer, initialState);
// ... بقية الكود
}
// مثال على كيفية استخدام dispatch
const fetchUsers = async () => {
dispatch({ type: 'FETCH_USERS_START' });
try {
const resp await fetch(`/api/users?page=${state.pagination.currentPage}&filter=${state.filter}`);
const data = await response.json();
dispatch({
type: 'FETCH_USERS_SUCCESS',
payload: { users: data.users, totalItems: data.totalItems }
});
} catch (error) {
dispatch({ type: 'FETCH_USERS_ERROR', payload: error.message });
}
};الميزة الرئيسية لـ useReducer هي أنه يجعل منطق تحديث الحالة أكثر قابلية للتنبؤ به وصيانة. بدلاً من وجود عدة setter functions مبعثرة في الكود، لديك مكان واحد (الـ reducer) يحدد كيفية تحديث الحالة بناءً على الـ action. هذا النهج مفيد بشكل خاص في المشاريع الكبيرة حيث تحتاج إلى تتبع كيفية تغير الحالة بمرور الوقت، أو عندما يكون لديك منطق تحديث معقد يتضمن عدة خطوات.
لكن useReducer ليس حلاً سحرياً. المشكلة الرئيسية هي أنه لا يحل مشكلة مشاركة الحالة بين المكونات البعيدة في شجرة المكونات. إذا حاولت استخدام useReducer لإدارة حالة مشتركة بين مكونات متعددة، فستجد نفسك تقوم بتمرير الـ state وdispatch عبر props، مما يؤدي إلى نفس مشكلة prop drilling التي حاولنا تجنبها. بالإضافة إلى ذلك، إذا كان لديك عدة reducers لإدارة حالات مختلفة، فقد ينتهي بك الأمر إلى تكرار منطق التحديث في أماكن متعددة، مما يجعل الكود أكثر صعوبة في الصيانة.
Context API هو الأداة التي يلجأ إليها المطورون عندما يريدون تجنب prop drilling، لكنه غالباً ما يتم استخدامه بشكل خاطئ. الفكرة الأساسية وراء Context هي توفير طريقة لمشاركة البيانات بين المكونات دون الحاجة إلى تمريرها عبر كل مستوى في شجرة المكونات. لكن المشكلة تكمن في أن معظم المطورين يستخدمون Context لإدارة الحالة بدلاً من مجرد مشاركتها، وهذا يؤدي إلى مشاكل في الأداء وصعوبة في الصيانة.
خلف الكواليس، Context API يعمل عن طريق إنشاء مرجع مشترك للبيانات في ذاكرة الـ Heap. عندما تقوم بإنشاء Context باستخدام React.createContext، فإن React يقوم بإنشاء كائن داخلي يحتوي على الـ Provider وConsumer. عندما تقوم بتحديث القيمة التي تمررها إلى الـ Provider، فإن React يقوم بإعادة تنفيذ جميع المكونات التي تستهلك هذا Context، حتى لو لم تكن هذه المكونات بحاجة إلى إعادة التنفيذ. هذا يعني أن استخدام Context لإدارة الحالة المتغيرة بشكل متكرر يمكن أن يؤدي إلى إعادة تنفيذ غير ضروري للمكونات، مما يؤثر سلباً على أداء التطبيق.
// مثال على سوء استخدام Context API
import React, { createContext, useState, useContext } from 'react';
const UserC createContext();
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
// هذا يؤدي إلى إعادة تنفيذ جميع المكونات التي تستهلك هذا Context
// عند تحديث أي من هذه الحالات
return (
<UserContext.Provider value={{ user, setUser, theme, setTheme, notifications, setNotifications }}>
{children}
</UserContext.Provider>
);
}
function UserProfile() {
const { user, setUser } = useContext(UserContext);
// هذا المكون يعاد تنفيذه عند تحديث theme أو notifications
// حتى لو لم يكن بحاجة إلى هذه التحديثات
return <div>{user?.name}</div>;
}
// الحل: تقسيم Context إلى عدة Contexts أصغر
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationsContext = createContext();القاعدة الأساسية لاستخدام Context هي: استخدمه لمشاركة البيانات التي لا تتغير بشكل متكرر، أو التي تحتاج إلى الوصول إليها من قبل العديد من المكونات في مستويات مختلفة من شجرة المكونات. على سبيل المثال، يمكنك استخدام Context لمشاركة معلومات المستخدم الحالية، أو إعدادات التطبيق، أو اللغة المختارة. لكن تجنب استخدامه لإدارة الحالة التي تتغير بشكل متكرر، مثل بيانات النموذج أو حالة التحميل، لأن هذا سيؤدي إلى إعادة تنفيذ غير ضروري للمكونات.
في أحد المشاريع التي عملت عليها، استخدمنا Context لإدارة حالة المستخدم الحالية وإعدادات التطبيق. لكن عندما حاولنا استخدام نفس Context لإدارة حالة البحث والفلترة في قائمة المنتجات، واجهنا مشكلة حيث كان كل تحديث لحالة البحث يؤدي إلى إعادة تنفيذ جميع المكونات التي تستهلك هذا Context، بما في ذلك مكونات القائمة الرئيسية التي لم تكن بحاجة إلى هذه التحديثات. الحل كان تقسيم Context إلى عدة Contexts أصغر، بحيث يكون لكل مجموعة من البيانات ذات الصلة Context خاص بها.
Redux هو أحد أكثر مكتبات إدارة الحالة شعبية في نظام React البيئي، لكنه أصبح مثيراً للجدل في السنوات الأخيرة. السبب الرئيسي هو أن Redux يتطلب الكثير من الـ boilerplate code، مما يجعل عملية التطوير أبطأ وأكثر تعقيداً. لكن هذا لا يعني أن Redux أصبح عديم الفائدة — بل يعني أنه يجب استخدامه بحكمة وفي المشاريع التي تستفيد حقاً من ميزاته الفريدة.
خلف الكواليس، Redux يعمل بناءً على ثلاثة مبادئ أساسية: الـ single source of truth، والـ state is read-only، والتغييرات تتم باستخدام pure functions. هذا يعني أن جميع حالات التطبيق مخزنة في كائن واحد (store)، ولا يمكن تغيير هذا الكائن مباشرة، بل يجب إرسال action يصف التغيير المطلوب، ويتم معالجة هذا الـ action باستخدام reducer function التي تعيد حالة جديدة. هذا النهج يجعل حالة التطبيق أكثر قابلية للتنبؤ به ويسهل تتبع كيفية تغير الحالة بمرور الوقت.
// مثال على إعداد Redux مع Redux Toolkit لتقليل boilerplate
import { configureStore, createSlice } from '@reduxjs/toolkit';
const usersSlice = createSlice({
name: 'users',
initialState: {
data: [],
filter: '',
sortBy: 'name',
isLoading: false,
error: null
},
reducers: {
fetchUsersStart(state) {
state.isLoading = true;
state.error = null;
},
fetchUsersSuccess(state, action) {
state.isLoading = false;
state.data = action.payload;
},
fetchUsersError(state, action) {
state.isLoading = false;
state.error = action.payload;
},
setFilter(state, action) {
state.filter = action.payload;
},
setSort(state, action) {
state.sortBy = action.payload;
}
}
});
export const { fetchUsersStart, fetchUsersSuccess, fetchUsersError, setFilter, setSort } = usersSlice.actions;
export const store = configureStore({
reducer: {
users: usersSlice.reducer
}
});
// استخدام في المكون
import { useSelector, useDispatch } from 'react-redux';
function UserList() {
const { data, filter, sortBy, isLoading, error } = useSelector(state => state.users);
const dispatch = useDispatch();
// ... بقية الكود
}الميزة الرئيسية لـ Redux هي أنه يوفر طريقة موحدة لإدارة الحالة في التطبيقات الكبيرة والمعقدة. إذا كان لديك تطبيق يحتوي على العديد من المكونات التي تحتاج إلى الوصول إلى نفس البيانات وتحديثها، فإن Redux يمكن أن يجعل الكود أكثر قابلية للصيانة والتنظيم. بالإضافة إلى ذلك، يوفر Redux أدوات قوية لتصحيح الأخطاء مثل Redux DevTools، والتي تسمح لك بتتبع كيفية تغير الحالة بمرور الوقت.
لكن Redux ليس مناسباً لكل مشروع. إذا كان تطبيقك صغيراً أو متوسط الحجم، فإن استخدام Redux قد يكون مبالغة ويضيف تعقيداً غير ضروري. بالإضافة إلى ذلك، يتطلب Redux الكثير من الـ boilerplate code، مما قد يبطئ عملية التطوير ويجعل الكود أكثر صعوبة في القراءة. في أحد المشاريع التي عملت عليها، استغرقنا ثلاثة أيام كاملة لإعداد Redux وإضافة أول ميزة، بينما كان بإمكاننا تنفيذ نفس الميزة في ساعتين باستخدام useReducer وContext.
Zustand هو مكتبة إدارة الحالة التي اكتسبت شعبية كبيرة في السنوات الأخيرة، خاصة بين المطورين الذين يبحثون عن بديل أخف وزناً وأكثر مرونة من Redux. الفكرة الأساسية وراء Zustand هي توفير طريقة بسيطة لإنشاء متاجر حالة يمكن الوصول إليها من أي مكان في التطبيق دون الحاجة إلى Context أو prop drilling.
خلف الكواليس، Zustand يعمل عن طريق إنشاء متجر حالة مركزي باستخدام الـ Closure في JavaScript. عندما تقوم بإنشاء متجر باستخدام Zustand، فإن المكتبة تقوم بإنشاء كائن داخلي يحتوي على الحالة الحالية وجميع الـ setter functions. يمكنك بعد ذلك الوصول إلى هذا المتجر من أي مكون باستخدام الـ useStore hook، والذي يقوم بإعادة تنفيذ المكون عند تغير الحالة. الفرق الرئيسي بين Zustand وRedux هو أن Zustand لا يتطلب كتابة reducers أو actions، مما يجعل الكود أكثر بساطة وأقل تعقيداً.
// مثال على استخدام Zustand لإدارة الحالة
import create from 'zustand';
const useUserStore = create((set) => ({
users: [],
filter: '',
sortBy: 'name',
isLoading: false,
error: null,
fetchUsers: async () => {
set({ isLoading: true, error: null });
try {
const resp await fetch('/api/users');
const data = await response.json();
set({ users: data, isLoading: false });
} catch (error) {
set({ error: error.message, isLoading: false });
}
},
setFilter: (filter) => set({ filter }),
setSort: (sortBy) => set({ sortBy })
}));
// استخدام في المكون
function UserList() {
const { users, filter, sortBy, isLoading, error, fetchUsers, setFilter, setSort } = useUserStore();
// ... بقية الكود
}
// ميزة Zustand: يمكنك تقسيم المتجر إلى عدة متاجر أصغر
const useAuthStore = create((set) => ({
user: null,
login: async (credentials) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
});
const user = await response.json();
set({ user });
},
logout: () => set({ user: null })
}));
function UserProfile() {
const { user } = useAuthStore();
// ... بقية الكود
}الميزة الرئيسية لـ Zustand هي بساطته ومرونته. بدلاً من كتابة الكثير من الـ boilerplate code كما هو الحال في Redux، يمكنك إنشاء متجر Zustand في بضع أسطر فقط. بالإضافة إلى ذلك، يوفر Zustand ميزات متقدمة مثل الـ middleware وdevtools، مما يجعله مناسباً للمشاريع الكبيرة والمعقدة. في أحد المشاريع التي عملت عليها، استخدمنا Zustand لإدارة حالة التطبيق بالكامل، بما في ذلك حالة المستخدم والبيانات الديناميكية وإعدادات التطبيق، وكان الأداء أفضل بكثير من استخدام Redux أو Context.
لكن Zustand ليس حلاً مثالياً لكل حالة. المشكلة الرئيسية هي أنه لا يفرض هيكلية محددة لإدارة الحالة، مما قد يؤدي إلى كود غير منظم إذا لم يتم استخدامه بحذر. بالإضافة إلى ذلك، على الرغم من أن Zustand أخف وزناً من Redux، إلا أنه لا يزال يضيف بعض التعقيد مقارنةً باستخدام useState أو useReducer لإدارة الحالة المحلية. إذا كنت تعمل على مشروع صغير أو متوسط الحجم ولا تحتاج إلى مشاركة الحالة بين العديد من المكونات، فقد يكون من الأفضل استخدام أدوات أبسط مثل useState أو useReducer.
الآن بعد أن استعرضنا جميع الأدوات الرئيسية لإدارة الحالة في React، حان الوقت لمقارنتها بناءً على عدة عوامل مهمة: سهولة الاستخدام، والأداء، وقابلية الصيانة، وملاءمتها للمشاريع المختلفة. الجدول التالي يلخص هذه المقارنة:
| الأداة | سهولة الاستخدام | الأداء | قابلية الصيانة | مناسب للمشاريع |
|----------------|------------------|-------------|------------------|--------------------------|
| useState | ★★★★★ | ★★★★☆ | ★★☆☆☆ | الصغيرة والمتوسطة |
| useReducer | ★★★☆☆ | ★★★★☆ | ★★★★☆ | المتوسطة والكبيرة |
| Context API | ★★★★☆ | ★★☆☆☆ | ★★★☆☆ | مشاركة البيانات الثابتة |
| Redux | ★★☆☆☆ | ★★★☆☆ | ★★★★★ | الكبيرة والمعقدة |
| Zustand | ★★★★☆ | ★★★★☆ | ★★★★☆ | المتوسطة والكبيرة |لكن هذه المقارنة ليست كافية لاتخاذ قرار. الحقيقة هي أن اختيار الأداة المناسبة يعتمد على عدة عوامل أخرى، مثل حجم الفريق، وخبرة المطورين، ومتطلبات الأداء، وتعقيد التطبيق. على سبيل المثال، إذا كنت تعمل بمفردك على مشروع صغير، فقد يكون استخدام useState وuseReducer كافياً تماماً. أما إذا كنت تعمل في فريق كبير على مشروع معقد، فقد يكون من الأفضل استخدام Zustand أو Redux لتوفير هيكلية موحدة لإدارة الحالة.
في تجربتي، أفضل استخدام مزيج من هذه الأدوات بدلاً من الاعتماد على أداة واحدة فقط. على سبيل المثال، يمكنك استخدام useState لإدارة الحالات المحلية البسيطة، وuseReducer لإدارة الحالات المعقدة داخل مكون واحد، وZustand لإدارة الحالات المشتركة بين المكونات، وContext لمشاركة البيانات الثابتة مثل إعدادات التطبيق. هذا النهج يوفر مرونة أكبر ويجعل الكود أكثر قابلية للصيانة.
لتبسيط عملية اتخاذ القرار، إليك خارطة طريق يمكنك اتباعها لاختيار أداة إدارة الحالة المناسبة لمشروعك:
بعد أكثر من عشر سنوات في تطوير تطبيقات React، تعلمت درساً مهماً واحداً: لا يوجد حل واحد يناسب الجميع في إدارة الحالة. الأداة التي تختارها يجب أن تعتمد على احتياجات مشروعك الفعلية، وليس على ما هو شائع أو ما استخدمته في الماضي. قبل أن تختار أداة لإدارة الحالة، اسأل نفسك هذه الأسئلة:
وأخيراً، تذكر أن إدارة الحالة ليست مجرد اختيار أداة — إنها فهم عميق لكيفية عمل React وJavaScript خلف الكواليس. مهما كانت الأداة التي تختارها، تأكد من أنك تفهم كيف تؤثر على أداء التطبيق وكيفية تصحيح الأخطاء عند حدوثها. في النهاية، الهدف ليس استخدام أحدث الأدوات أو أكثرها شعبية، بل بناء تطبيقات سريعة ومستقرة وسهلة الصيانة.
إدارة الحالة في React ليست عن الأدوات، بل عن فهم متى تستخدم ماذا ولماذا. الأداة الصحيحة هي تلك التي تجعل الكود أكثر قابلية للصيانة دون التضحية بالأداء.
— مهندس برمجيات سنيور في Nouvil