أفضل مخفضات مع Immer

نشرت: 2022-03-10
ملخص سريع ↬ في هذه المقالة ، سوف نتعلم كيفية استخدام Immer لكتابة مخفضات السرعة. عند العمل مع React ، نحافظ على الكثير من الحالة. لإجراء تحديثات على حالتنا ، نحتاج إلى كتابة الكثير من المخفضات. تؤدي الكتابة اليدوية للمخفضات إلى رمز متضخم حيث يتعين علينا لمس كل جزء من دولتنا تقريبًا. هذا أمر ممل وعرضة للخطأ. في هذه المقالة ، سنرى كيف يضفي Immer مزيدًا من البساطة على عملية كتابة مخفضات الحالة.

بصفتك مطور React ، يجب أن تكون على دراية بالمبدأ القائل بأنه لا ينبغي تغيير الحالة بشكل مباشر. قد تتساءل عما يعنيه ذلك (كان لدى معظمنا هذا الارتباك عندما بدأنا).

هذا البرنامج التعليمي سينصف ذلك: ستفهم ماهية الحالة غير القابلة للتغيير والحاجة إليها. ستتعلم أيضًا كيفية استخدام Immer للعمل مع الحالة الثابتة وفوائد استخدامها. يمكنك العثور على الكود في هذه المقالة في Github repo.

الثبات في JavaScript ولماذا يهم

Immer.js هي مكتبة جافا سكريبت صغيرة كتبها Michel Weststrate وتتمثل مهمتها المعلنة في السماح لك "بالعمل مع الحالة الثابتة بطريقة أكثر ملاءمة".

ولكن قبل الغوص في Immer ، دعنا نحصل بسرعة على تجديد معلومات حول الثبات في JavaScript وسبب أهميتها في تطبيق React.

يحدد أحدث معيار ECMAScript (المعروف أيضًا باسم JavaScript) تسعة أنواع بيانات مضمنة. من بين هذه الأنواع التسعة ، هناك ستة يشار إليها باسم القيم / الأنواع primitive . هذه الأوليات الستة هي undefined ، وهي number ، و string ، و boolean ، و bigint ، و symbol . سيكشف فحص بسيط باستخدام عامل تشغيل typeof عن أنواع أنواع البيانات هذه.

 console.log(typeof 5) // number console.log(typeof 'name') // string console.log(typeof (1 < 2)) // boolean console.log(typeof undefined) // undefined console.log(typeof Symbol('js')) // symbol console.log(typeof BigInt(900719925474)) // bigint

القيمة primitive هي قيمة ليست كائنًا وليس لها طرق. الأهم في مناقشتنا الحالية هو حقيقة أن القيمة الأولية لا يمكن تغييرها بمجرد إنشائها. وهكذا ، يُقال إن الأوليات immutable .

الأنواع الثلاثة المتبقية هي null object function . يمكننا أيضًا التحقق من أنواعها باستخدام عامل التشغيل typeof .

 console.log(typeof null) // object console.log(typeof [0, 1]) // object console.log(typeof {name: 'name'}) // object const f = () => ({}) console.log(typeof f) // function

هذه الأنواع mutable . هذا يعني أنه يمكن تغيير قيمها في أي وقت بعد إنشائها.

قد تتساءل عن سبب وجود المصفوفة [0, 1] هناك. حسنًا ، في JavaScriptland ، المصفوفة هي ببساطة نوع خاص من الكائنات. في حال كنت تتساءل أيضًا عن null وكيف يختلف عن undefined . undefined تعني ببساطة أننا لم نقم بتعيين قيمة لمتغير بينما القيمة null هي حالة خاصة للكائنات. إذا كنت تعرف أن شيئًا ما يجب أن يكون كائنًا ولكن الكائن غير موجود ، فأنت ببساطة ترجع null .

للتوضيح بمثال بسيط ، حاول تشغيل الكود أدناه في وحدة تحكم المتصفح.

 console.log('aeiou'.match(/[x]/gi)) // null console.log('xyzabc'.match(/[x]/gi)) // [ 'x' ]

يجب أن String.prototype.match مصفوفة ، وهي نوع object . عندما لا يمكن العثور على مثل هذا الكائن ، فإنه يعود null . العودة undefined لن تكون منطقية هنا أيضًا.

يكفي ذلك. دعنا نعود إلى مناقشة الثبات.

المزيد بعد القفز! أكمل القراءة أدناه ↓

وفقًا لمستندات MDN:

"تحدد جميع الأنواع باستثناء الكائنات القيم الثابتة (أي القيم التي لا يمكن تغييرها)."

تتضمن هذه العبارة وظائف لأنها نوع خاص من كائن JavaScript. انظر تعريف الوظيفة هنا.

دعونا نلقي نظرة سريعة على ما تعنيه أنواع البيانات المتغيرة وغير القابلة للتغيير في الممارسة العملية. حاول تشغيل الكود أدناه في وحدة تحكم المتصفح الخاص بك.

 let a = 5; let b = a console.log(`a: ${a}; b: ${b}`) // a: 5; b: 5 b = 7 console.log(`a: ${a}; b: ${b}`) // a: 5; b: 7

تظهر نتائجنا أنه على الرغم من أن b "مشتق" من a ، فإن تغيير قيمة b لا يؤثر على قيمة a . ينشأ هذا من حقيقة أنه عندما ينفذ محرك JavaScript العبارة b = a ، فإنه ينشئ موقعًا جديدًا ومنفصلًا للذاكرة ، ويضع 5 فيه ، ويشير b في ذلك الموقع.

ماذا عن الأشياء؟ النظر في الكود أدناه.

 let c = { name: 'some name'} let d = c; console.log(`c: ${JSON.stringify(c)}; d: ${JSON.stringify(d)}`) // {"name":"some name"}; d: {"name":"some name"} d.name = 'new name' console.log(`c: ${JSON.stringify(c)}; d: ${JSON.stringify(d)}`) // {"name":"new name"}; d: {"name":"new name"}

يمكننا أن نرى أن تغيير خاصية الاسم عبر المتغير d يغيرها أيضًا في c . ينشأ هذا من حقيقة أنه عندما ينفذ محرك JavaScript العبارة ، c = { name: 'some name ' } ، ينشئ محرك JavaScript مساحة في الذاكرة ، ويضع الكائن بداخله ، ويشير c إليه. بعد ذلك ، عند تنفيذ العبارة d d = c ، يشير محرك JavaScript فقط إلى نفس الموقع. لا يقوم بإنشاء موقع ذاكرة جديد. وبالتالي ، فإن أي تغييرات على العناصر الواردة في d تعتبر ضمنيًا عملية على العناصر الموجودة في c . بدون بذل الكثير من الجهد ، يمكننا أن نرى لماذا هذا هو مشكلة في صنع.

تخيل أنك تقوم بتطوير تطبيق React وفي مكان ما تريد إظهار اسم المستخدم some name من خلال القراءة من المتغير c . لكن في مكان آخر أدخلت خطأً في شفرتك عن طريق معالجة الكائن d . سيؤدي هذا إلى ظهور اسم المستخدم كاسم new name . إذا كان c و d من الأوليات ، فلن نواجه هذه المشكلة. لكن الأوليات بسيطة جدًا لأنواع الحالات التي يجب على تطبيق React النموذجي الحفاظ عليها.

يتعلق هذا بالأسباب الرئيسية التي تجعل من المهم الحفاظ على حالة ثابتة في طلبك. أشجعك على التحقق من بعض الاعتبارات الأخرى من خلال قراءة هذا القسم القصير من Immutable.js README: حالة الثبات.

بعد أن فهمنا سبب حاجتنا إلى الثبات في تطبيق React ، دعنا الآن نلقي نظرة على كيفية معالجة Immer للمشكلة من خلال وظيفة produce الخاصة به.

وظيفة produce الغمر

تعد واجهة برمجة تطبيقات Immer الأساسية صغيرة جدًا ، والوظيفة الرئيسية التي ستعمل بها هي وظيفة produce . يأخذ produce ببساطة حالة أولية واستدعاء يحدد كيف يجب أن تتغير الحالة. يستقبل رد النداء نفسه نسخة مسودة (متطابقة ، لكن لا تزال نسخة) من الحالة التي يتم فيها إجراء كل التحديث المقصود. أخيرًا ، produce حالة جديدة غير قابلة للتغيير مع تطبيق جميع التغييرات.

النمط العام لهذا النوع من تحديث الحالة هو:

 // produce signature produce(state, callback) => nextState

دعونا نرى كيف يعمل هذا في الممارسة.

 import produce from 'immer' const initState = { pets: ['dog', 'cat'], packages: [ { name: 'react', installed: true }, { name: 'redux', installed: true }, ], } // to add a new package const newPackage = { name: 'immer', installed: false } const nextState = produce(initState, draft => { draft.packages.push(newPackage) })

في الكود أعلاه ، نقوم ببساطة بتمرير حالة البداية واستدعاء يحدد كيف نريد حدوث الطفرات. إنها بهذه السهولة. لسنا بحاجة للمس أي جزء آخر من الدولة. إنه يترك initState كما هو ويشارك هيكليًا تلك الأجزاء من الدولة التي لم نلمسها بين الحالة الأولى والحالة الجديدة. أحد هذه الأجزاء في ولايتنا هو مجموعة pets . produce d nextState عبارة عن شجرة حالة ثابتة تحتوي على التغييرات التي أجريناها بالإضافة إلى الأجزاء التي لم نعدّلها.

مسلحين بهذه المعرفة البسيطة والمفيدة ، دعنا نلقي نظرة على كيف يمكن produce أن يساعدنا في تبسيط مخفضات React.

الكتابة مخفضات مع Immer

افترض أن لدينا كائن الحالة المحدد أدناه

 const initState = { pets: ['dog', 'cat'], packages: [ { name: 'react', installed: true }, { name: 'redux', installed: true }, ], };

وأردنا إضافة كائن جديد ، وفي خطوة لاحقة ، اضبط مفتاحه installed على true

 const newPackage = { name: 'immer', installed: false };

إذا أردنا القيام بذلك بالطريقة المعتادة باستخدام كائن JavaScripts وبناء جملة انتشار المصفوفة ، فقد يبدو مخفض الحالة الخاص بنا كما يلي.

 const updateReducer = (state = initState, action) => { switch (action.type) { case 'ADD_PACKAGE': return { ...state, packages: [...state.packages, action.package], }; case 'UPDATE_INSTALLED': return { ...state, packages: state.packages.map(pack => pack.name === action.name ? { ...pack, installed: action.installed } : pack ), }; default: return state; } };

يمكننا أن نرى أن هذا مطول بشكل غير ضروري وعرضة للأخطاء في كائن الحالة البسيط نسبيًا هذا. علينا أيضًا أن نلمس كل جزء من أجزاء الدولة ، وهو أمر غير ضروري. دعونا نرى كيف يمكننا تبسيط ذلك باستخدام Immer.

 const updateReducerWithProduce = (state = initState, action) => produce(state, draft => { switch (action.type) { case 'ADD_PACKAGE': draft.packages.push(action.package); break; case 'UPDATE_INSTALLED': { const package = draft.packages.filter(p => p.name === action.name)[0]; if (package) package.installed = action.installed; break; } default: break; } });
وباستخدام بضعة أسطر من التعليمات البرمجية ، قمنا بتبسيط المخفض بشكل كبير. أيضًا ، إذا وقعنا في الحالة الافتراضية ، فسيقوم Immer بإرجاع حالة المسودة دون الحاجة إلى القيام بأي شيء. لاحظ كيف أن هناك عددًا أقل من التعليمات البرمجية المعيارية والقضاء على انتشار الحالة. مع Immer ، نحن نهتم فقط بجزء الدولة الذي نريد تحديثه. إذا لم نتمكن من العثور على مثل هذا العنصر ، كما هو الحال في إجراء "UPDATE_INSTALLED" ، فنحن ببساطة ننتقل دون لمس أي شيء آخر. تتناسب وظيفة "الإنتاج" أيضًا مع الكاري. يُقصد تمرير رد نداء كأول وسيط لـ "إنتاج" لاستخدامه في الكاري. توقيع "المنتج" بالكاري هو
 //curried produce signature produce(callback) => (state) => nextState
دعونا نرى كيف يمكننا تحديث حالتنا السابقة بمنتج كاري. سيبدو إنتاجنا بالكاري كما يلي:
 const curriedProduce = produce((draft, action) => { switch (action.type) { case 'ADD_PACKAGE': draft.packages.push(action.package); break; case 'SET_INSTALLED': { const package = draft.packages.filter(p => p.name === action.name)[0]; if (package) package.installed = action.installed; break; } default: break; } });

تقبل دالة الإنتاج curried وظيفة باعتبارها الوسيطة الأولى لها وتعيد منتجًا محمصًا لا يتطلب الآن سوى حالة يتم من خلالها إنتاج الحالة التالية. الحجة الأولى للوظيفة هي حالة المسودة (والتي ستشتق من الحالة التي سيتم تمريرها عند استدعاء هذا المنتج الكاريكاتير). ثم يتبع كل عدد من الحجج التي نرغب في تمريرها إلى الوظيفة.

كل ما علينا فعله الآن لاستخدام هذه الوظيفة هو المرور بالحالة التي نريد إنتاج الحالة التالية وكائن الإجراء مثل ذلك.

 // add a new package to the starting state const nextState = curriedProduce(initState, { type: 'ADD_PACKAGE', package: newPackage, }); // update an item in the recently produced state const nextState2 = curriedProduce(nextState, { type: 'SET_INSTALLED', name: 'immer', installed: true, });

لاحظ أنه في تطبيق React عند استخدام الخطاف useReducer ، لا نحتاج إلى تمرير الحالة صراحة كما فعلت أعلاه لأنها تعتني بذلك.

قد تتساءل ، هل سيحصل Immer على hook ، مثل كل شيء في React هذه الأيام؟ حسنًا ، أنت بصحبة أخبار جيدة. يحتوي Immer على خطافين للعمل مع الحالة: useImmer و useImmerReducer . دعونا نرى كيف يعملون.

استخدام useImmer و useImmerReducer

أفضل وصف useImmer يأتي من README أثناء الاستخدام نفسه.

useImmer(initialState) تشبه إلى حد بعيد useState . تُرجع الدالة tuple ، القيمة الأولى للمجموعة هي الحالة الحالية ، والثانية هي وظيفة المُحدِث ، التي تقبل وظيفة مُنتِجة غامرة ، حيث يمكن تغيير draft بحرية ، حتى ينتهي المُنتِج وسيتم إجراء التغييرات غير قابل للتغيير وتصبح الدولة التالية.

للاستفادة من هذه الخطافات ، يجب عليك تثبيتها بشكل منفصل ، بالإضافة إلى مكتبة Immer الرئيسية.

 yarn add immer use-immer

من حيث التعليمات البرمجية ، يبدو خطاف useImmer كما يلي

 import React from "react"; import { useImmer } from "use-immer"; const initState = {} const [ data, updateData ] = useImmer(initState)

وهي بهذه البساطة. يمكنك القول إنها حالة استخدام React ولكن مع القليل من الستيرويد. استخدام وظيفة التحديث بسيط للغاية. يتلقى حالة المسودة ويمكنك تعديلها بالقدر الذي تريده أدناه.

 // make changes to data updateData(draft => { // modify the draft as much as you want. })

قدم مُنشئ Immer مثالاً على الأكواد وصندوق يمكنك اللعب به لترى كيف يعمل.

useImmerReducer بالمثل إذا كنت قد استخدمت خطاف خفض استخدام useReducer . لها توقيع مماثل. دعونا نرى كيف يبدو ذلك من حيث التعليمات البرمجية.

 import React from "react"; import { useImmerReducer } from "use-immer"; const initState = {} const reducer = (draft, action) => { switch(action.type) { default: break; } } const [data, dataDispatch] = useImmerReducer(reducer, initState);

يمكننا أن نرى أن المخفض يتلقى حالة draft يمكننا تعديلها بقدر ما نريد. يوجد أيضًا مثال على الأكواد وعلبة الصوت هنا لتجربته.

وهذا هو مدى سهولة استخدام خطافات Immer. ولكن إذا كنت لا تزال تتساءل عن سبب استخدام Immer في مشروعك ، فإليك ملخصًا لبعض أهم الأسباب التي وجدتها لاستخدام Immer.

لماذا يجب عليك استخدام Immer

إذا كنت قد كتبت منطق إدارة الحالة لأي فترة زمنية ، فستقدر بسرعة بساطة عروض Immer. لكن هذه ليست الميزة الوحيدة التي يقدمها إيمير.

عند استخدام Immer ، ينتهي بك الأمر إلى كتابة كود برمجي أقل كما رأينا باستخدام مخفضات بسيطة نسبيًا. هذا أيضًا يجعل التحديثات العميقة سهلة نسبيًا.

مع مكتبات مثل Immutable.js ، عليك أن تتعلم واجهة برمجة تطبيقات جديدة لجني فوائد الثبات. ولكن مع Immer ، يمكنك تحقيق نفس الشيء باستخدام Objects JavaScript العادية ، Arrays ، Sets ، Maps . لا يوجد شيء جديد لنتعلمه.

يوفر Immer أيضًا مشاركة هيكلية بشكل افتراضي. هذا يعني ببساطة أنه عند إجراء تغييرات على كائن الحالة ، يشارك Immer تلقائيًا الأجزاء غير المتغيرة من الحالة بين الحالة الجديدة والحالة السابقة.

مع Immer ، تحصل أيضًا على تجميد تلقائي للكائن مما يعني أنه لا يمكنك إجراء تغييرات على الحالة produced . على سبيل المثال ، عندما بدأت في استخدام Immer ، حاولت تطبيق طريقة sort على مجموعة من الكائنات التي تم إرجاعها بواسطة وظيفة إنتاج Immer. ألقى خطأً يخبرني أنه لا يمكنني إجراء أي تغييرات على المصفوفة. اضطررت إلى تطبيق طريقة شريحة المصفوفة قبل تطبيق sort . مرة أخرى ، فإن nextState هي شجرة حالة ثابتة.

Immer أيضًا مكتوب بقوة وهو صغير جدًا عند 3 كيلو بايت فقط عند الضغط على gzip.

خاتمة

عندما يتعلق الأمر بإدارة تحديثات الحالة ، فإن استخدام Immer لا يحتاج إلى تفكير بالنسبة لي. إنها مكتبة خفيفة الوزن للغاية تتيح لك الاستمرار في استخدام كل الأشياء التي تعلمتها عن JavaScript دون محاولة تعلم شيء جديد تمامًا. أشجعك على تثبيته في مشروعك والبدء في استخدامه على الفور. يمكنك إضافة استخدامه في المشاريع الحالية وتحديث مخفضاتك بشكل تدريجي.

أود أيضًا أن أشجعك على قراءة منشور مدونة Immer التمهيدي بواسطة Michael Weststrate. الجزء الذي أجده مثيرًا للاهتمام بشكل خاص هو "كيف يعمل Immer؟" الذي يشرح كيف تستفيد Immer من ميزات اللغة مثل الوكلاء والمفاهيم مثل النسخ عند الكتابة.

أود أيضًا أن أشجعك على إلقاء نظرة على منشور المدونة هذا: الثبات في JavaScript: A Contratian View حيث يعرض المؤلف ، Steven de Salas ، أفكاره حول مزايا متابعة الثبات.

آمل أنه من خلال الأشياء التي تعلمتها في هذا المنشور ، يمكنك البدء في استخدام Immer على الفور.

موارد ذات الصلة

  1. use-immer ، جيثب
  2. إمر ، جيثب
  3. function ، مستندات الويب MDN ، Mozilla
  4. proxy ، مستندات الويب MDN ، Mozilla
  5. كائن (علوم الكمبيوتر) ، ويكيبيديا
  6. "الثبات في JS ،" Orji Chidi Matthew ، GitHub
  7. "أنواع وقيم بيانات ECMAScript" ، Ecma International
  8. مجموعات غير قابلة للتغيير لـ JavaScript و Immutable.js و GitHub
  9. "قضية الثبات" ، Immutable.js ، GitHub