React و Redux و Immutable.js: مكونات لتطبيقات الويب الفعالة
نشرت: 2022-03-11تعد React و Redux و Immutable.js حاليًا من بين مكتبات JavaScript الأكثر شيوعًا وأصبحت بسرعة الخيار الأول للمطورين عندما يتعلق الأمر بتطوير الواجهة الأمامية. في عدد قليل من مشاريع React و Redux التي عملت عليها ، أدركت أن الكثير من المطورين الذين بدأوا مع React لا يفهمون تمامًا React وكيفية كتابة تعليمات برمجية فعالة للاستفادة من إمكاناتها الكاملة.
في هذا البرنامج التعليمي Immutable.js ، سننشئ تطبيقًا بسيطًا باستخدام React و Redux ، ونحدد بعضًا من أكثر إساءة استخدام React شيوعًا وطرق تجنبها.
مشكلة مرجع البيانات
React هو كل شيء عن الأداء. تم إنشاؤه من الألف إلى الياء ليكون عالي الأداء ، فقط يعيد عرض أجزاء صغيرة من DOM لتلبية التغييرات الجديدة في البيانات. يجب أن يتكون أي تطبيق React في الغالب من مكونات صغيرة بسيطة (أو وظيفة عديمة الحالة). من السهل التفكير فيها ويمكن لمعظمهم أن يكون لديهم دالة shouldComponentUpdate تعيد القيمة false .
shouldComponentUpdate(nextProps, nextState) { return false; }
من ناحية الأداء ، فإن أهم وظيفة لدورة حياة المكون هي shouldComponentUpdate وإذا أمكن ، يجب أن ترجع دائمًا false . يضمن هذا عدم إعادة تصيير هذا المكون أبدًا (باستثناء التصيير الأولي) بشكل فعال مما يجعل تطبيق React سريعًا للغاية.
عندما لا يكون الأمر كذلك ، فإن هدفنا هو إجراء فحص رخيص للمساواة بين الدعائم / الحالة القديمة مقابل الدعائم / الحالة الجديدة وتخطي إعادة العرض إذا لم تتغير البيانات.
لنعد خطوة للوراء لثانية ونراجع كيفية قيام JavaScript بإجراء فحوصات المساواة لأنواع البيانات المختلفة.
يعد فحص المساواة لأنواع البيانات الأولية مثل منطقية وسلسلة وعدد صحيح أمرًا بسيطًا للغاية حيث تتم مقارنتها دائمًا بقيمتها الفعلية:
1 === 1 'string' === 'string' true === true
من ناحية أخرى ، فإن التحقق من المساواة لأنواع معقدة مثل الكائنات والمصفوفات والوظائف مختلف تمامًا. كائنان متماثلان إذا كان لهما نفس المرجع (يشير إلى نفس الكائن في الذاكرة).
const obj1 = { prop: 'someValue' }; const obj2 = { prop: 'someValue' }; console.log(obj1 === obj2); // false
على الرغم من أن obj1 و obj2 يبدوان متطابقين ، إلا أن مرجعهما مختلف. نظرًا لاختلافهما ، فإن مقارنتها بسذاجة داخل دالة shouldComponentUpdate ستؤدي إلى إعادة تصيير المكون دون داع.
الشيء المهم الذي يجب ملاحظته هو أن البيانات القادمة من مخفضات Redux ، إذا لم يتم إعدادها بشكل صحيح ، سيتم تقديمها دائمًا بمرجع مختلف مما يؤدي إلى إعادة عرض المكون في كل مرة.
هذه مشكلة أساسية في سعينا لتجنب إعادة عرض المكونات.
معالجة المراجع
لنأخذ مثالاً لدينا فيه كائنات متداخلة بعمق ونريد مقارنتها بالإصدار السابق. يمكننا إجراء حلقة متكررة من خلال دعائم الكائن المتداخلة ومقارنة كل واحدة منها ، ولكن من الواضح أن ذلك سيكون مكلفًا للغاية وغير وارد.
هذا يترك لنا حلًا واحدًا فقط ، وهو التحقق من المرجع ، ولكن تظهر مشكلات جديدة بسرعة:
- الحفاظ على المرجع إذا لم يتغير شيء
- تغيير المرجع إذا تم تغيير أي من قيم خاصية الكائن / الصفيف المتداخلة
هذه ليست مهمة سهلة إذا أردنا القيام بها بطريقة لطيفة ونظيفة ومحسّنة الأداء. أدرك Facebook هذه المشكلة منذ وقت طويل ودعا Immutable.js إلى الإنقاذ.
import { Map } from 'immutable'; // transform object into immutable map let obj1 = Map({ prop: 'someValue' }); const obj2 = obj1; console.log(obj1 === obj2); // true obj1 = obj1.set('prop', 'someValue'); // set same old value console.log(obj1 === obj2); // true | does not break reference because nothing has changed obj1 = obj1.set('prop', 'someNewValue'); // set new value console.log(obj1 === obj2); // false | breaks reference
لا تؤدي أي من وظائف Immutable.js طفرة مباشرة على البيانات المعينة. بدلاً من ذلك ، يتم استنساخ البيانات داخليًا وتحولها وإذا كانت هناك أي تغييرات يتم إرجاع مرجع جديد. وإلا فإنه يقوم بإرجاع المرجع الأولي. يجب تعيين المرجع الجديد بشكل صريح ، مثل obj1 = obj1.set(...);
.
أمثلة على React و Redux و Immutable.js
أفضل طريقة لإثبات قوة هذه المكتبات هي إنشاء تطبيق بسيط. وماذا يمكن أن يكون أبسط من تطبيق المهام؟
للإيجاز ، في هذه المقالة ، سنستعرض فقط أجزاء التطبيق المهمة لهذه المفاهيم. يمكن العثور على الكود المصدري الكامل لشفرة التطبيق على GitHub.
عند بدء تشغيل التطبيق ، ستلاحظ أن المكالمات إلى console.log يتم وضعها بشكل ملائم في المناطق الرئيسية لإظهار مقدار إعادة تصيير DOM بوضوح ، وهو الحد الأدنى.
مثل أي تطبيق todo آخر ، نريد عرض قائمة بعناصر المهام. عندما ينقر المستخدم على عنصر المهام ، سنضع علامة عليه على أنه مكتمل. نحتاج أيضًا إلى حقل إدخال صغير في الأعلى لإضافة todos جديدة وفي المرشحات الثلاثة السفلية التي ستسمح للمستخدم بالتبديل بين:
- الجميع
- مكتمل
- نشيط
مخفض الإرجاع
تعيش جميع البيانات في تطبيق Redux داخل كائن تخزين واحد ويمكننا النظر إلى المخفضات على أنها مجرد طريقة ملائمة لتقسيم المتجر إلى أجزاء أصغر يسهل التفكير فيها. نظرًا لأن المخفض هو أيضًا وظيفة ، فيمكن تقسيمه أيضًا إلى أجزاء أصغر.
يتكون المخفض الخاص بنا من جزأين صغيرين:
- عمل قائمة
- عامل التصفية النشط
// reducers/todos.js import * as types from 'constants/ActionTypes'; // we can look at List/Map as immutable representation of JS Array/Object import { List, Map } from 'immutable'; import { combineReducers } from 'redux'; function todoList(state = List(), action) { // default state is empty List() switch (action.type) { case types.ADD_TODO: return state.push(Map({ // Every switch/case must always return either immutable id: action.id, // or primitive (like in activeFilter) state data text: action.text, // We let Immutable decide if data has changed or not isCompleted: false, })); // other cases... default: return state; } } function activeFilter(state = 'all', action) { switch (action.type) { case types.CHANGE_FILTER: return action.filter; // This is primitive data so there's no need to worry default: return state; } } // combineReducers combines reducers into a single object // it lets us create any number or combination of reducers to fit our case export default combineReducers({ activeFilter, todoList, });
الاتصال مع Redux
الآن بعد أن أعددنا مخفض Redux ببيانات Immutable.js ، دعنا نربطه بمكوِّن React لتمرير البيانات.
// components/App.js import { connect } from 'react-redux'; // ….component code const mapStateToProps = state => ({ activeFilter: state.todos.activeFilter, todoList: state.todos.todoList, }); export default connect(mapStateToProps)(App);
في عالم مثالي ، يجب إجراء الاتصال فقط على مكونات مسار المستوى الأعلى ، واستخراج البيانات في mapStateToProps والباقي عبارة عن دعائم تمرير React الأساسية للأطفال. في التطبيقات واسعة النطاق ، يكون من الصعب تتبع جميع الاتصالات ، لذلك نريد تقليلها إلى الحد الأدنى.
من المهم جدًا ملاحظة أن state.todos هو كائن JavaScript عادي يتم إرجاعه من وظيفة Redux combeducers (todos هي اسم المخفض) ، لكن state.todos.todoList هي قائمة غير قابلة للتغيير ومن المهم أن تبقى في مثل هذا شكل حتى يمر يجب التحقق من تحديث مكون.
تجنب إعادة تصيير المكونات
قبل أن نتعمق أكثر ، من المهم أن نفهم نوع البيانات التي يجب تقديمها للمكون:
- أنواع بدائية من أي نوع
- كائن / مجموعة فقط في شكل ثابت
يسمح لنا وجود هذه الأنواع من البيانات بمقارنة العناصر التي تأتي في مكونات React بشكل سطحي.
يوضح المثال التالي كيفية تمييز الدعائم بأبسط طريقة ممكنة:
$ npm install react-pure-render
import shallowEqual from 'react-pure-render/shallowEqual'; shouldComponentUpdate(nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); }
سوف تقوم الوظيفة الضحلة المساواة بالتحقق من اختلاف الدعائم / الحالة بعمق مستوى واحد فقط. إنه يعمل بسرعة كبيرة وهو في تآزر مثالي مع بياناتنا غير القابلة للتغيير. قد يكون الاضطرار إلى كتابة هذا shouldComponentUpdate في كل مكون أمرًا غير مريح للغاية ، ولكن لحسن الحظ يوجد حل بسيط.

استخراج shouldComponentUpdate في مكون منفصل خاص:
// components/PureComponent.js import React from 'react'; import shallowEqual from 'react-pure-render/shallowEqual'; export default class PureComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); } }
ثم قم فقط بتمديد أي مكون يكون فيه منطق shouldComponentUpdate مطلوبًا:
// components/Todo.js export default class Todo extends PureComponent { // Component code }
هذه طريقة نظيفة وفعالة للغاية لتجنب إعادة عرض المكونات في معظم الحالات ، وبعد ذلك إذا أصبح التطبيق أكثر تعقيدًا وتطلب فجأة حلًا مخصصًا ، فيمكن تغييره بسهولة.
توجد مشكلة بسيطة عند استخدام PureComponent أثناء تمرير الوظائف كدعامات. نظرًا لأن React ، مع فئة ES6 ، لا تربط هذا تلقائيًا بالوظائف التي يتعين علينا القيام بها يدويًا. يمكننا تحقيق ذلك من خلال القيام بأحد الإجراءات التالية:
- استخدم ربط دالة سهم ES6:
<Component onClick={() => this.handleClick()} />
- استخدم الربط :
<Component onClick={this.handleClick.bind(this)} />
سيؤدي كلا الأسلوبين إلى إعادة تصيير المكون لأنه تم تمرير مرجع مختلف إلى onClick في كل مرة.
للتغلب على هذه المشكلة ، يمكننا ربط الدوال مسبقًا في طريقة المُنشئ مثل:
constructor() { super(); this.handleClick = this.handleClick.bind(this); } // Then simply pass the function render() { return <Component onClick={this.handleClick} /> }
إذا وجدت نفسك تقوم بربط وظائف متعددة مسبقًا في معظم الأوقات ، فيمكننا تصدير وإعادة استخدام وظيفة المساعد الصغيرة:
// utils/bind-functions.js export default function bindFunctions(functions) { functions.forEach(f => this[f] = this[f].bind(this)); } // some component constructor() { super(); bindFunctions.call(this, ['handleClick']); // Second argument is array of function names }
إذا لم يعمل أي من الحلول من أجلك ، فيمكنك دائمًا كتابة شروط shouldComponentUpdate يدويًا.
معالجة البيانات غير القابلة للتغيير داخل أحد المكونات
مع إعداد البيانات الحالي غير القابل للتغيير ، تم تجنب إعادة التصيير وتركنا مع بيانات غير قابلة للتغيير داخل دعائم المكون. هناك عدد من الطرق لاستخدام هذه البيانات غير القابلة للتغيير ، ولكن الخطأ الأكثر شيوعًا هو تحويل البيانات على الفور إلى JS عادي باستخدام وظيفة toJS غير القابلة للتغيير.
استخدام toJS لتحويل البيانات غير القابلة للتغيير بعمق إلى JS عادي ينفي الغرض الكامل من تجنب إعادة العرض لأنه كما هو متوقع ، يكون بطيئًا جدًا وبالتالي يجب تجنبه. إذن كيف نتعامل مع البيانات غير القابلة للتغيير؟
يجب استخدامه كما هو ، ولهذا السبب توفر واجهة برمجة التطبيقات غير القابلة للتغيير مجموعة متنوعة من الوظائف ، والخريطة ، ويتم استخدامها بشكل شائع داخل مكون React. بنية بيانات todoList القادمة من Redux Reducer عبارة عن مجموعة من الكائنات في شكل غير قابل للتغيير ، يمثل كل كائن عنصرًا واحدًا للقيام به:
[{ id: 1, text: 'todo1', isCompleted: false, }, { id: 2, text: 'todo2', isCompleted: false, }]
واجهة برمجة تطبيقات Immutable.js مشابهة جدًا لجافا سكريبت العادي ، لذلك سنستخدم todoList مثل أي مصفوفة أخرى من الكائنات. تثبت وظيفة الخريطة بشكل أفضل في معظم الحالات.
داخل استدعاء خريطة نحصل على todo ، وهو كائن لا يزال في شكل ثابت ويمكننا تمريره بأمان في مكون Todo .
// components/TodoList.js render() { return ( // …. {todoList.map(todo => { return ( <Todo key={todo.get('id')} todo={todo}/> ); })} // …. ); }
إذا كنت تخطط لإجراء تكرارات متعددة متسلسلة على بيانات غير قابلة للتغيير مثل:
myMap.filter(somePred).sort(someComp)
... فمن المهم جدًا تحويله أولاً إلى Seq باستخدام toSeq وبعد التكرار ، أعده إلى الشكل المطلوب مثل:
myMap.toSeq().filter(somePred).sort(someComp).toOrderedMap()
نظرًا لأن Immutable.js لا يغير البيانات المعينة بشكل مباشر ، فإنه يحتاج دائمًا إلى عمل نسخة أخرى منها ، وقد يكون إجراء تكرارات متعددة مثل هذا مكلفًا للغاية. Seq عبارة عن تسلسل كسول غير قابل للتغيير للبيانات ، مما يعني أنه سيؤدي أقل عدد ممكن من العمليات للقيام بمهمته أثناء تخطي إنشاء نسخ وسيطة. تم إنشاء التسلسل ليتم استخدامه بهذه الطريقة.
داخل مكون Todo ، استخدم get أو getIn للحصول على الدعائم.
بسيط بما فيه الكفاية ، أليس كذلك؟
حسنًا ، ما أدركته هو أنه في كثير من الأحيان يمكن أن يصبح غير قابل للقراءة للغاية مع وجود عدد كبير من get()
وخاصة getIn()
. لذلك قررت أن أجد بقعة جيدة بين الأداء وسهولة القراءة وبعد بعض التجارب البسيطة اكتشفت أن الدالتين Immutable.js toObject و toArray تعملان بشكل جيد للغاية.
تقوم هذه الوظائف بتحويل كائنات / مصفوفات Immutable.js (مستوى 1 عميق) إلى كائنات / مصفوفات JavaScript عادية. إذا كان لدينا أي بيانات متداخلة بعمق في الداخل ، فستبقى في شكل ثابت وجاهز لتمريرها
إنه أبطأ من get()
بهامش ضئيل ، لكنه يبدو أكثر نظافة:
// components/Todo.js render() { const { id, text, isCompleted } = this.props.todo.toObject(); // ….. }
دعونا نرى كل شيء في العمل
في حالة عدم استنساخ الرمز من GitHub حتى الآن ، فقد حان الوقت الآن للقيام بذلك:
git clone https://github.com/rogic89/ToDo-react-redux-immutable.git cd ToDo-react-redux-immutable
بدء تشغيل الخادم أمر بسيط (تأكد من تثبيت Node.js و NPM) على النحو التالي:
npm install npm start
انتقل إلى http: // localhost: 3000 في متصفح الويب الخاص بك. مع فتح وحدة تحكم المطور ، شاهد السجلات وأنت تضيف بعض عناصر المهام ، وقم بتمييزها على أنها تم وتغيير عامل التصفية:
- أضف 5 أشياء يجب القيام بها
- قم بتغيير الفلتر من "الكل" إلى "نشط" ثم العودة إلى "الكل"
- لا توجد إعادة تصيير ، ما عليك سوى تغيير التصفية
- ضع علامة على 2 من العناصر التي يجب القيام بها على أنها مكتملة
- تم إعادة عرض اثنين من المهام ، ولكن واحدة فقط في كل مرة
- قم بتغيير الفلتر من "الكل" إلى "نشط" ثم العودة إلى "الكل"
- تم تركيب / إلغاء تثبيت 2 فقط من عناصر المهام المكتملة
- لم يتم إعادة عرض العناصر النشطة
- احذف بند واحد من "المهام المطلوبة" من منتصف القائمة
- تأثر عنصر المهام الذي تمت إزالته فقط ، ولم تتم إعادة عرض العناصر الأخرى
يتم إحتوائه
يوفر تآزر React و Redux و Immutable.js ، عند استخدامه بشكل صحيح ، بعض الحلول الأنيقة للعديد من مشكلات الأداء التي غالبًا ما تصادف في تطبيقات الويب الكبيرة.
يسمح لنا Immutable.js باكتشاف التغييرات في كائنات / مصفوفات JavaScript دون اللجوء إلى عدم كفاءة عمليات فحص المساواة العميقة ، والتي بدورها تسمح لـ React بتجنب عمليات إعادة التصيير الباهظة عندما لا تكون مطلوبة. هذا يعني أن أداء Immutable.js يميل إلى أن يكون جيدًا في معظم السيناريوهات.
أتمنى أن تكون المقالة قد أعجبتك ووجدتها مفيدة في بناء حلول React المبتكرة في مشاريعك المستقبلية.