الفرق بين Redux, Context & React Components في إدارة الحالة
أغسطس 6, 2025عند بناء تطبيقات باستخدام React، هناك حاجة إلى إدارة البيانات التي تتغير نتيجة تفاعل المستخدم أو جلب البيانات من مصادر خارجية. توجد ثلاث وسائل رئيسية لإدارة الحالة داخل تطبيق React: الحالة المحلية داخل المكونات (React Components)، وواجهة السياق (Context API)، ومكتبة Redux. كل وسيلة لها طريقة استخدام خاصة وتناسب سيناريوهات معينة حسب حجم وتعقيد التطبيق.
React Components – إدارة الحالة المحلية
في React، كل مكوّن (Component) يمكنه إدارة حالته الداخلية الخاصة باستخدام هوك useState ضمن المكوّن نفسه. هذه الحالة المحلية (Local State) تناسب الحالات البسيطة التي تخص مكوّنًا واحدًا فقط. على سبيل المثال، مكوّن زر يعدّ عدد النقرات عليه يمكن أن يحتفظ بهذا العدد في حالته المحلية:
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
تم الضغط على الزر {count} مرات
</button>
);
}
في هذا المثال، يستخدم المكوّن Counter حالة محلية count يتم تغييرها بواسطة الدالة setCount عند الضغط على الزر. يتم إعادة عرض (re-render) المكوّن كلما تغيرت قيمة count، مما يؤدي إلى تحديث النص المعروض على الزر.
ميزة هذا النهج هي بساطته وسهولة فهمه. لكنه يصبح أقل فعالية عندما تحتاج عدة مكوّنات إلى مشاركة نفس البيانات. في حالة وجود مكوّنات متعددة تحتاج إلى الوصول إلى نفس الحالة، سنضطر لتمرير تلك البيانات عبر خصائص المكوّنات (props) من مكوّن لآخر. هذه العملية تُعرف باسم props drilling، أي تمرير الخصائص عبر عدة طبقات من المكوّنات. props drilling قد يؤدي إلى تعقيد الشفرة وصعوبة متابعتها، خاصة إذا كانت شجرة المكوّنات عميقة أو كان عدد المكوّنات كبيرًا.
React Context – مشاركة البيانات بين المكوّنات
عندما يكون لدينا بيانات نرغب في مشاركتها بين عدة مكوّنات دون اللجوء لتمريرها عبر كل مكوّن وسيط، يمكن استخدام واجهة Context API المضمنة في React. يتيح Context إنشاء مزود بيانات (Provider) يلتف حول شجرة من المكوّنات ويمكّن لجميع تلك المكوّنات الداخلية من الوصول إلى بيانات معينة مباشرة باستخدام هوك useContext.
عادة ما يُستخدم Context مع بيانات عالمية التشارك مثل: معلومات المستخدم الحالي، إعدادات اللغة، سمات التصميم (مثل الوضع الداكن والفاتح)، إلخ. خطوات استخدام Context تكون كالتالي:
- إنشاء Context: يتم ذلك عبر استدعاء
React.createContextوتخزين الناتج. يمكن تحديد قيمة افتراضية مبدئية للـ Context. - توفير القيمة عبر Provider: يتم التفاف جزء من التطبيق أو كله بمكوّن
Providerالخاص بالـ Context الذي أنشأناه، ونمرّر البيانات (القيمة) المراد مشاركتها عبر الخاصيةvalueفي الـProvider. - استهلاك القيمة في المكوّنات الأبناء: داخل أي مكوّن واقع ضمن الشجرة المغلّفة بالـ
Provider، يمكن استخدام هوكuseContext(MyContext)للحصول مباشرة على القيمة المخزّنة والمشارَكة في الـ Context.
فيما يلي مثال مبسط لإنشاء Context واستخدامه لمشاركة قيمة سمة التصميم (الثيم) بين مكوّنين:
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Content />
</ThemeContext.Provider>
);
}
function Content() {
const currentTheme = useContext(ThemeContext);
return <p>الوضع الحالي: {currentTheme}</p>;
}
في الكود أعلاه، قمنا بإنشاء Context باسم ThemeContext بقيمة افتراضية 'light'. بعد ذلك، يحتوي المكوّن App على ThemeContext.Provider الذي يوفّر القيمة 'dark' لكل المكوّنات داخله (هنا نفترض وجود مكوّن Content كجزء من الشجرة). المكوّن Content يستخدم useContext(ThemeContext) للحصول على قيمة الثيم الحالية وعرضها.
يُسهّل Context مشاركة البيانات دون الحاجة للتمرير عبر كل مستويات شجرة المكوّنات، مما يحل مشكلة prop drilling في كثير من الحالات. ومع ذلك، ينبغي الانتباه إلى أن استخدام Context قد يؤدي إلى إعادة رسم جميع المكوّنات المستهلكة للقيمة عند حدوث أي تغيير في تلك القيمة. لذلك، إذا كانت البيانات تتغير بشكل متكرر أو كانت بحجم كبير، قد تتأثر أداء التطبيق سلبًا باستخدام Context فقط. في التطبيقات الكبيرة أو الحالات التي تتطلب تحديثات متكررة للبيانات المشتركة، قد يكون استخدام Redux أكثر فعالية من حيث التحكم في الأداء وتنظيم التحديثات.
Redux – إدارة الحالة بشكل مركزي للتطبيقات الكبيرة
Redux هي مكتبة خارجية شائعة لإدارة الحالة في تطبيقات JavaScript، بما في ذلك React. توفر Redux نهجًا مركزيًا لحفظ حالة التطبيق كاملة في كائن واحد يُسمى store (المخزن). تتميز بأنه لا يمكن تغيير الحالة (البيانات) داخل هذا المخزن إلا من خلال عمليات مُحددة تُسمى Actions يتم معالجتها بواسطة وظائف تُسمى Reducers.
فكرة Redux الأساسية هي وجود store مركزي يحتوي على الحالة الشاملة للتطبيق، ويمكن لأي مكوّن الاشتراك (subscribe) للحصول على أجزاء من هذه الحالة أو إرسال أحداث (Actions) لتغييرها. باستخدام Redux، يتم فصل منطق تحديث الحالة عن المكوّنات، مما يجعل تتبع التغييرات في البيانات أسهل عبر أدوات تطوير مثل Redux DevTools.
لنفترض أن لدينا تطبيقًا معقدًا يحتوي على العديد من البيانات المتداخلة (مثل معلومات مستخدم، عناصر سلة مشتريات، إعدادات تطبيق...). في هذه الحالة، استخدام Redux يساعد في تنظيم البيانات وتحديثها بشكل متسق. حاليًا، الطريقة الموصى بها لاستخدام Redux مع React هي من خلال حزمة Redux Toolkit التي تُبسّط الكثير من الإعدادات وتوفّر أدوات حديثة. على سبيل المثال، يمكننا استخدام دالة createSlice من Redux Toolkit لإنشاء منطق حالة يخص جزءًا معينًا من التطبيق (نسمي هذا الجزء slice):
import { configureStore, createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1 },
decrement: (state) => { state.value -= 1 }
}
});
const store = configureStore({
reducer: { counter: counterSlice.reducer }
});
في الكود أعلاه، أنشأنا slice باسم "counter" يحتوي على حالة بسيطة (قيمة رقمية تبدأ من 0) وإجرائيّن (increment و decrement) لتحديث هذه الحالة. يقوم createSlice تلقائيًا بتوليد دوال الإجراء (action creators) التي تحمل نفس أسماء المُعالجين (reducers) لتعريف كيفية تعديل الحالة. بعد ذلك، أنشأنا store باستخدام configureStore ومرّرنا إليه (reducer) الخاص بالعداد ضمن كائن (reducers).
الآن أصبح لدينا store مركزي يحوي حالة التطبيق. لربط هذا المخزن بتطبيق React، نقوم بلفّ التطبيق بالمكوّن Provider المصدّر من مكتبة react-redux وتمرير store إليه، عادةً في ملف الدخول الرئيسي (مثل index.js):
import { Provider } from 'react-redux';
import App from './App';
import store from './store';
root.render(
<Provider store={store}>
<App />
</Provider>
);
بعد هذه الخطوة، يستطيع أي مكوّن داخل التطبيق الوصول إلى حالة Redux أو إرسال Actions لتحديثها. يوفر react-redux أداتين هوك تسهّلان التعامل مع Redux داخل المكوّنات الوظيفية: useSelector للاشتراك وقراءة جزء من الحالة، و useDispatch لإرسال Action معين. باستخدام هذين الهوكين، يمكننا إنشاء مكوّن واجهة يستخدم حالة Redux كما يلي:
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './counterSlice';
function CounterDisplay() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<p>القيمة: {count}</p>
<button onClick={() => dispatch(increment())}>زيادة</button>
<button onClick={() => dispatch(decrement())}>نقصان</button>
</div>
);
}
في المثال أعلاه، يستخدم المكوّن CounterDisplay دالة useSelector للحصول على قيمة العداد الحالية من state.counter.value داخل المخزن. ويستخدم useDispatch للحصول على وظيفة dispatch التي تسمح بإرسال Action لتحديث الحالة عند الضغط على الأزرار. تم جلب دوال increment و decrement المُصدّرة تلقائيًا من الـ slice الخاص بالعداد لاستخدامها في إرسال الـ Actions.
يتميز Redux بعدة أمور تجعل إدارة الحالة في التطبيقات الكبيرة أكثر سهولة وتنظيمًا:
- يقدم هيكلية موحّدة ومنظمة لحفظ البيانات وتحديثها، مما يجعل الشفرة أكثر قابلية للتنبؤ والفهم.
- يسهل تتبع تغيرات الحالة عبر أدوات تطوير مثل Redux DevTools التي تمكن المطور من رؤية سجل الإجراءات وتغيرات الحالة خطوة بخطوة.
- يعتمد Redux على مبدأ الحالة غير القابلة للتغيير (immutable state)؛ أي أن التحديثات لا تتم بتغيير الكائن مباشرة وإنما بإنتاج حالة جديدة، مما يقلل الأخطاء الناجمة عن تعديل الحالة في أماكن متعددة.
- يدعم بشكل ممتاز التعامل مع العمليات غير المتزامنة (مثل طلبات API) عن طريق أدوات مثل
createAsyncThunkفي Redux Toolkit أو مكاتب وسطية (middlewares) مثلredux-thunk، مما يسهل إدارة حالة البيانات أثناء عمليات الجلب والتحديث غير المتزامن. - يُمكّن ربط المكوّنات بجزء معين من الحالة (باستخدام
useSelector) بحيث لا يعاد رسم إلا المكوّنات التي تتأثر بالجزء المتغير من الحالة، وهذا يساعد في تحسين الأداء مقارنة باستخدام Context بمفرده في الحالات المعقدة.
على الرغم من قوة Redux، فإنه يضيف طبقة من التعقيد والكود الإضافي. لذلك ﻻ تستخدم Redux في التطبيقات الصغيرة أو الحالات التي لا تكون فيها إدارة الحالة معقدة. في مثل هذه الحالات، قد يكون استخدام useState أو Context كافيًا ويحقق الغرض بدون تعقيد إضافي. بشكل عام، Redux مفيد جدًا عندما يكبر حجم التطبيق وتتعدد الحالات المتداخلة وتحتاج إلى مركزية وتنسيق في إدارتها.
اختيار الأداة المناسبة لإدارة الحالة
- الحالة المحلية (Local State) في المكوّن: مناسبة للبيانات البسيطة التي تخص مكوّنًا واحدًا فقط ولا تحتاج للمشاركة.
- Context API: مناسبة لمشاركة بيانات على نطاق أوسع قليلاً من مجرد مكوّن واحد، خاصة إذا كانت البيانات شبه ثابتة أو لا تتغير بشكل متكرر.
- Redux: مناسب للتطبيقات الكبيرة والمعقدة حيث توجد الكثير من الحالات المتشابكة أو المتغيرة باستمرار. إذا كان التطبيق يحتوي على أجزاء متعددة من الحالة بحاجة إلى تزامن، أو يتطلب أدوات تتبع وتحكم أعلى (مثل التراجع عن التغييرات، مراقبة العمليات، تخزين مركزي للبيانات المشتركة)، فإن Redux يكون الخيار الأنسب.
كقاعدة عامة، ابدأ بالأبسط. يمكنك استخدام الحالة المحلية useState طالما أنها تلبي الغرض. إذا بدأت تشعر بالحاجة إلى تمرير بيانات عبر مكوّنات متعددة بشكل متعب، انتقل إلى Context لتبسيط المشاركة. وإذا نما التطبيق أكثر وأصبح من الصعب إدارة الحالة الموزعة أو احتجت قدرات أكثر تعقيدًا للتحكم في البيانات (مثل سجل تغيير الحالة أو تنظيم عمليات الجلب المتعددة)، يمكن حينها إدخال Redux إلى المشروع.
إدارة الحالة في React تعتبر جزءًا أساسيًا من بناء التطبيقات التفاعلية. يوفر React أدوات مختلفة لهذا الغرض بدءًا من الإمكانيات المدمجة (الحالة المحلية و Context) وصولًا إلى حلول خارجية متقدمة مثل Redux. كل أداة من هذه الأدوات لها نقاط قوة ونقاط ضعف. المفتاح هو فهم متطلبات تطبيقك بشكل واضح ثم اختيار النهج الأنسب: استخدم الحل الأبسط الممكن الذي يغطي احتياجاتك، ولا تلجأ إلى التعقيد إلا عندما يكون ضروريًا. باستخدام النهج المناسب، ستتمكن من بناء تطبيقات React قابلة للصيانة والتوسعة، مع الحفاظ على أداء جيد وتجربة مستخدم سلسة.
المدونة
يوليو 26, 2025
1. البدايات: النشأة داخل Facebook في عام 2011، واجه مهندسو Facebook تحديًا متزايدًا في صيانة واجهات المستخدم المعقدة والتفاعلية. تم تطوير مكتبة د...
يوليو 02, 2025
تجاوز $fillable بأمان باستخدام forceFill() في Laravel جربت تستخدم create() في Laravel ولاقيت إن حقول زي role أو status مش بتتحفظ؟ ده بسبب حماية Larav...
يوليو 16, 2025
🌐 إتقان CSS الحديثة: قوة if()، Popover Hints، والتنسيقات الذكية CSS دلوقتي بقت أذكى بكتير. دالة if()، وميزة popover="hint"، وازاي نكتب تنسيقات متج...
يوليو 31, 2025
Hook useCallback في React يُستخدم لإنشاء نسخة ثابتة من دالة (callback) بحيث لا يُعاد إنشاؤها إلا إذا تغيرت الـ dependencies المحددة. الهدف الأساسي م...
يوليو 07, 2025
Laravel 12.19: استخدام Attributes تنظّم Query Builder في Laravel 12.19، دلوقتي تقدر تستخدم Attribute اسمه #[UseEloquentBuilder] علشان تحدد Query Bu...
يوليو 27, 2025
🔍 ما هي Array.fromAsync() بالضبط؟ Array.fromAsync() هي دالة static من كائن Array، شبيهة بالدالة Array.from()، لكن الفرق الجوهري هو أن Array.fromAs...