فهم وإصلاح مشكلة عدم إعادة عرض مكونات React عند تغير الـ Props
مقدمة: تحديات إعادة عرض المكونات في React مع Redux
في عالم تطوير واجهات المستخدم باستخدام مكتبة React، يُعد ضمان تحديث المكونات وعرضها بشكل صحيح عند تغير بياناتها (props أو state) أمرًا جوهريًا. ومع ذلك، قد يواجه المطورون أحيانًا سيناريوهات محيرة حيث تتغير البيانات في مخزن Redux، ولكن المكونات المرتبطة بها لا تُعاد عرضها (re-render) بالشكل المتوقع. هذه المشكلة شائعة بشكل خاص عند التعامل مع تدفقات البيانات غير المتزامنة والتفاعلات بين المكونات الأب والابن.
يهدف هذا المقال إلى استكشاف أحد هذه السيناريوهات، وتحليل الأسباب الكامنة وراء عدم إعادة العرض، وتقديم حلول عملية لضمان تحديث واجهة المستخدم بسلاسة وفعالية، مع التركيز على أفضل الممارسات في React و Redux.
السيناريو المطروح: مكون أب ومكون ابن وتحديث غير متوقع
لنتخيل مشروع React و Redux يتضمن مكونين رئيسيين: مكون أب (Parent Component) ومكون ابن (Child Component). يقوم المكون الأب بتمرير بعض الـ props إلى المكون الابن. عندما يستقبل المكون الابن هذه الـ props، فإنه يقوم باستدعاء إجراء (Redux action) لتعبئة جزء من هذه الـ props بشكل غير متزامن (asynchronously) من خلال تحديث مخزن Redux.
المشكلة تكمن في أن مخزن Redux يتم تحديثه بنجاح، ولكن المكون الابن لا يُعاد عرضه ليعكس هذه التغييرات، مما يؤدي إلى واجهة مستخدم غير متزامنة مع الحالة الفعلية للبيانات. دعونا نلقي نظرة على بنية المكونين:
مكون الأب (Parent Component)
class Parent extends React . Component {
getChilds(){
let child = [];
let i = 0 ;
for ( let keys in this .props.home.data) {
child[i] = (
< Child title = {keys} data = {this.props.home.data[keys]} key = {keys} />
);
i++;
if (i === 6 ) break ;
}
return child;
}
render(){
return (
< div >
< h1 > I am gonna call my child </ h1 >
{this.getChilds()}
</ div >
)
}
}
يُظهر هذا المكون الأب كيفية تمرير البيانات من الـ props الخاصة به إلى مكونات الأبناء (Child) التي ينشئها داخل دالة getChilds().
مكون الابن (Child Component)
class Child extends React . Component {
componentDidMount(){
if ( this .props.data.items.length === 0 ){
// calling an action to fill this.props.data.items array with data
this .props.getData( this .props.data.id);
}
}
getGrandSon(){
let grandSons = [];
if ( this .props.data.items.length > 0 ){
grandSons = this .props.data.items.map( item => < GrandSon item = {item} /> );
}
return grandSons;
}
render(){
return (
< div >
< h1 > I am the child component and I will call my own child </ h1 &n
{this.getGrandSon()}
</ div >
)
}
}
في هذا المكون الابن، نلاحظ أن دالة componentDidMount() تتحقق مما إذا كانت مصفوفة this.props.data.items فارغة. إذا كانت فارغة، فإنها تستدعي إجراء Redux (this.props.getData) لجلب البيانات وتعبئة هذه المصفوفة. المشكلة هي أنه على الرغم من تحديث مخزن Redux بشكل صحيح بالبيانات الجديدة، فإن المكون Child لا يُعاد عرضه ليعكس هذه التغييرات.
تحليل المشكلة: لماذا لا يتم إعادة العرض؟
لفهم سبب عدم إعادة عرض المكون Child، يجب أن نتعمق في كيفية عمل React و Redux معًا:
آلية عرض React والمقارنة السطحية (Shallow Comparison)
تعتمد React على آلية فعالة لتحديد متى يجب إعادة عرض المكونات. بشكل افتراضي، يقوم React بإعادة عرض المكون عندما تتغير الـ props أو الـ state الخاصة به. ومع ذلك، فإن طريقة مقارنة هذه التغييرات هي المفتاح.
- المقارنة السطحية (
Shallow Comparison): عند مقارنة الكائنات أو المصفوفات في الـpropsأو الـstate، لا يقومReact(أو مكتبات مثلreact-redux) بإجراء مقارنة عميقة (deep comparison) لمحتويات الكائن أو المصفوفة. بدلاً من ذلك، فإنه يتحقق مما إذا كانت المرجع (reference) للكائن أو المصفوفة قد تغير. إذا كان المرجع هو نفسه، حتى لو تغيرت البيانات الداخلية، فإنReactقد يفترض عدم وجود تغيير حقيقي ويتخطى عملية إعادة العرض. - حالة السيناريو: في حالتنا، عندما يتم تحديث مخزن
Reduxبالبيانات الجديدة، قد لا يتم إنشاء مرجع جديد لمصفوفةthis.props.data.itemsإذا كانت عملية التحديث تتم بطريقة تحافظ على المرجع الأصلي للكائنdataأو المصفوفةitemsضمن هذا الكائن.
دور Redux و mapStateToProps
تستخدم مكتبة react-redux دالة connect() (أو useSelector في المكونات الوظيفية) لربط مكونات React بمخزن Redux. تقوم دالة mapStateToProps بتحديد أي جزء من حالة Redux يجب أن يتم تمريره كـ props إلى المكون.
- عدم الكشف عن التغيير: إذا كانت دالة
mapStateToPropsتعيد نفس مرجع الكائن للـpropsحتى بعد تحديث البيانات في مخزنRedux، فإنreact-reduxلن يكتشف تغييرًا في الـprops، وبالتالي لن يطلب منReactإعادة عرض المكون. - نمط خاطئ (
Anti-pattern): من المهم الإشارة إلى أنه عادةً ما لا يكون من مسؤولية المكون الابن (Child) تعبئة البيانات التي من المفترض أن يستقبلها جاهزة من المكون الأب (Parent). يجب أن يتدفق البيانات بشكل أحادي الاتجاه (unidirectional data flow) من الأب إلى الابن. ومع ذلك، قد يكون من المفيد تحديث ومعالجة البيانات من مكون الابن، خاصة عند استخدامRedux.
الحلول المقترحة لضمان إعادة العرض
للتغلب على مشكلة عدم إعادة العرض، يمكننا تطبيق بعض الحلول التي تضمن اكتشاف React للتغييرات وإعادة عرض المكونات بشكل صحيح.
تحسين mapStateToProps لضمان تحديث الـ Props
أحد الحلول الفعالة هو التأكد من أن دالة mapStateToProps تعيد دائمًا كائنات جديدة (new objects) أو مصفوفات جديدة (new arrays) عندما تتغير البيانات في مخزن Redux. هذا يضمن أن يكون مرجع الـ props مختلفًا، مما يجبر React على إعادة العرض.
يمكن تحقيق ذلك عن طريق إعادة بناء الكائن الذي يتم إرجاعه من mapStateToProps، كما هو موضح في هذا المثال:
const mapStateToProps = state => {
return {
id : state.data.id,
items : state.data.items
}
}
في هذا المثال، بدلاً من تمرير الكائن state.data بأكمله، نقوم بإنشاء كائن جديد يحتوي على خصائص id و items بشكل منفصل. إذا تغيرت قيمة state.data.items (أي أصبحت مصفوفة جديدة)، فإن الكائن الجديد الذي يتم إرجاعه من mapStateToProps سيحتوي على مرجع جديد للمصفوفة items، مما سيؤدي إلى اكتشاف react-redux للتغيير وإعادة عرض المكون Child.
أفضل الممارسات لتدفق البيانات
بينما يعمل الحل السابق كـ workaround فعال، من المهم أيضًا التفكير في أفضل الممارسات لتصميم تدفق البيانات في تطبيقات React و Redux:
- مسؤولية جلب البيانات: يفضل أن يكون المكون الأب (
Parent) أو مكونات المستوى الأعلى (higher-order components) هي المسؤولة عن جلب البيانات وتمريرها جاهزة إلى المكونات الأبناء. هذا يضمن تدفق بيانات نظيفًا وأحادي الاتجاه. - استخدام
useEffectأوcomponentDidUpdate: إذا كان المكون الابن يحتاج حقًا إلى التفاعل مع تغييرات معينة في الـpropsبعد التركيب الأولي (initial mount)، يمكن استخدام خطافuseEffectفي المكونات الوظيفية (مع مصفوفة تبعيات مناسبة) أو دالةcomponentDidUpdateفي المكونات الفئوية (class components) لتنفيذ منطق معين عند تغير الـprops. - التعامل مع الثوابت (
Immutability): فيRedux، من الضروري دائمًا تحديث الحالة بطريقة غير قابلة للتغيير (immutable way)، أي عن طريق إنشاء كائنات ومصفوفات جديدة بدلاً من تعديل الكائنات والمصفوفات الموجودة. هذا يضمن أن المقارنات السطحية تعمل بشكل صحيح.
الخلاصة التقنية
تُعد مشكلة عدم إعادة عرض مكونات React عند تغير الـ props تحديًا شائعًا، خاصة عند دمجها مع إدارة الحالة باستخدام Redux والتعامل مع البيانات غير المتزامنة. يكمن جوهر المشكلة غالبًا في آلية المقارنة السطحية (shallow comparison) التي تستخدمها React و react-redux، والتي قد لا تكتشف التغييرات إذا ظل مرجع الكائن أو المصفوفة هو نفسه، حتى لو تغيرت البيانات الداخلية.
لضمان تحديث واجهة المستخدم بشكل موثوق، يجب على المطورين التركيز على إعادة بناء الكائنات في دالة mapStateToProps لضمان إرجاع مراجع جديدة عند تحديث البيانات. بالإضافة إلى ذلك، فإن الالتزام بأفضل الممارسات في تدفق البيانات، مثل جعل المكونات الأبوية مسؤولة عن جلب البيانات وتمريرها جاهزة، واستخدام تحديثات الحالة غير القابلة للتغيير (immutable updates) في Redux، سيساهم بشكل كبير في بناء تطبيقات React قوية وسهلة الصيانة وذات أداء عالٍ.