لقد استخدمنا حتى اللحظة مخزن Redux بمساعدة واجهة خطافات أمنتها المكتبة react-redux. وقد استخدمنا بالتحديد الدالتين useSelector وuseDispatch.
ولنكمل هذا القسم علينا الاطلاع على طريقة أقدم وأكثر تعقيدًا لاستخدام Redux، وهي استخدام الدالة connect التي تؤمنها المكتبة Redux.
عليك قطعًا استخدام واجهة الخطافات البرمجية عندما تنشئ تطبيقات جديدة، لكن معرفة طريقة عمل connect
أمر ضروري عند صيانة مشاريع Redux أقدم.
استخدام الدالة connect لمشاركة مخزن Redux بين المكوِّنات
لنعدّل المكوِّن Notes
بحيث يستخدم الدالة connect
بدلًا من واجهة الخطافات البرمجية (بدلًا من استخدام الدالتين useSelector وuseDispatch).علينا تعديل الإجزاء التالية من المكوِّن:
import React from 'react' import { useDispatch, useSelector } from 'react-redux'import { toggleImportanceOf } from '../reducers/noteReducer' const Notes = () => { const dispatch = useDispatch() const notes = useSelector(({filter, notes}) => { if ( filter === 'ALL' ) { return notes } return filter === 'IMPORTANT' ? notes.filter(note => note.important) : notes.filter(note => !note.important) }) return( <ul> {notes.map(note => <Note key={note.id} note={note} handleClick={() => dispatch(toggleImportanceOf(note.id)) } /> )} </ul> ) } export default Notes
يمكن استخدام الدالة connect
لتحويل مكونات React "النظامية" بحيث يتصل المكوِّن بحالة مخزن Redux عن طريق خصائصه. لنستخدم أولًا الدالة connect
لتحويل المكوّن Notes
إلى مكوّن متّصل (connected component):
import React from 'react' import { connect } from 'react-redux'import { toggleImportanceOf } from '../reducers/noteReducer' const Notes = () => { // ... } const ConnectedNotes = connect()(Notes) export default ConnectedNotes
تُصدِّر الوحدة المكوِّن المتصل والذي سيعمل حاليًا كالمكوّن النظامي السابق تمامًا. يحتاج المكوِّن إلى قائمة بالملاحظات وإلى قيمة المُرشِّح من مخزن Redux. تستقبل الدالة connect
دالة أخرى تدعى mapStateToProps كمعامل أول. حيث تُستخدم هذه الأخيرة في تعريف خصائص المكوِّن المتصل والتي تعتمد على حالة مخزن Redux. لو كتبنا الشيفرة التالية:
const Notes = (props) => { const dispatch = useDispatch() const notesToShow = () => { if ( props.filter === 'ALL ') { return props.notes } return props.filter === 'IMPORTANT' ? props.notes.filter(note => note.important) :props.notes.filter(note => !note.important) } return( <ul> {notesToShow().map(note => <Note key={note.id} note={note} handleClick={() => dispatch(toggleImportanceOf(note.id)) } /> )} </ul> ) } const mapStateToProps = (state) => { return { notes: state.notes, filter: state.filter, } } const ConnectedNotes = connect(mapStateToProps)(Notes) export default ConnectedNotes
يمكن للمكون أن يلج حالة المخزن مباشرة من خلال الأمرprops.notes
والذي يعطينا قائمة الملاحظات. كما يمكن الوصول إلى قيمة المرشِّح من خلال الأمر props.filter
. كما يمكن توضيح الحالة التي تنتج عن استخدام الدالة connect
مع الدالة mapStateToProps
التي عرفناها كما يلي:
ويمكن أيضًا للمكوِّن Notes
أن يصل مباشرة إلى المخزن بالطريقة التي ذكرناها سابقًا لغرض التحري والتفتيش. لا يحتاج المكوّن NoteList
عمليًا إلى أية معلومات حول المُرشِّح المختار، لذلك يمكن نقل منطق عملية الانتقاء إلى مكان آخر. وعلينا فقط تقديم الملاحظات المنتقاة بشكل صحيح للمكوّن من خلال الخاصية note
.
const Notes = (props) => { const dispatch = useDispatch() return( <ul> {props.notes.map(note => <Note key={note.id} note={note} handleClick={() => dispatch(toggleImportanceOf(note.id)) } /> )} </ul> ) } const mapStateToProps = (state) => { if ( state.filter === 'ALL' ) { return { notes: state.notes } } return { notes: (state.filter === 'IMPORTANT' ? state.notes.filter(note => note.important) : state.notes.filter(note => !note.important) ) }} const ConnectedNotes = connect(mapStateToProps)(Notes) export default ConnectedNotes
الدالة mapDispatchToProps
لقد تخلصنا الآن من الدالة useSelector
، لكن المكوّن Note
لايزال يستخدم الخطاف useDispatch
ودالة الإيفاد التي تستخدمه:
const Notes = (props) => { const dispatch = useDispatch() return( <ul> {props.notes.map(note => <Note key={note.id} note={note} handleClick={() => dispatch(toggleImportanceOf(note.id)) } /> )} </ul> ) }
يستخدم المعامل الثاني للدالة connect
لتعريف الدالة mapDispatchToProps، ويمثل هذا المعامل مجموعة من الدوال المولدة للأفعال، تُمرّر إلى الدالة connect
كخاصية. لنجري التعديلات التالية على عملية الاتصال التي أنشأناها:
const mapStateToProps = (state) => { return { notes: state.notes, filter: state.filter, } } const mapDispatchToProps = { toggleImportanceOf,} const ConnectedNotes = connect( mapStateToProps, mapDispatchToProps)(Notes) export default ConnectedNotes
يستطيع المكوّن الآن إيفاد الفعل الذي يعّرفه مولد الأفعال toggleImportanceOf
مباشرةً، عن طريق طلب الدالة من خلال خصائصها:
const Notes = (props) => { return( <ul> {props.notes.map(note => <Note key={note.id} note={note} handleClick={() => props.toggleImportanceOf(note.id)} /> )} </ul> ) }
أي بدلًا من إيفاد الفعل بالطريقة التالية:
dispatch(toggleImportanceOf(note.id))
يمكننا ببساطة إنجاز ذلك باستخدام connect
:
props.toggleImportanceOf(note.id)
لا حاجة لطلب الدالة dispatch
بشكل منفصل، طالما أنّ الدالة connect
قد عدلت مولد الفعل toggleImportanceOf
إلى الشكل الذي يحتوي dispatch
.
سيتطلب منك الأمر بعض الوقت لتقلب في ذهنك الطريقة التي تعمل بها الدالة mapDispatchToProps
، وخاصة عندما سنلقي نظرة على طرق بديلة لاستخدامها لاحقًا في هذا الفصل.
يمكن إظهار النتيجة التي سنحصل عليها باستخدام connect
من خلال الشكل التالي:
وبالإضافة إلى قدرته على الولوج إلى حالة المخزن، يمكن للمكوِّن الإشارة إلى دالة يمكن أن تستخدم لإيفاد أفعال من النوع TOGGLE_IMPORTANCE
من خلال الخاصية toggleImportanceOf
.
تمثل الشيفرة التالية المكوّن Notes
وقد كُتب من جديد:
import React from 'react' import { connect } from 'react-redux' import { toggleImportanceOf } from '../reducers/noteReducer' const Notes = (props) => { return( <ul> {props.notes.map(note => <Note key={note.id} note={note} handleClick={() => props.toggleImportanceOf(note.id)} /> )} </ul> ) } const mapStateToProps = (state) => { if ( state.filter === 'ALL' ) { return { notes: state.notes } } return { notes: (state.filter === 'IMPORTANT' ? state.notes.filter(note => note.important) : state.notes.filter(note => !note.important) ) } } const mapDispatchToProps = { toggleImportanceOf } export default connect( mapStateToProps, mapDispatchToProps )(Notes)
لنستخدم أيضًا الدالة connect
لإنشاء ملاحظة جديدة:
import React from 'react' import { connect } from 'react-redux' import { createNote } from '../reducers/noteReducer' const NewNote = (props) => { const addNote = async (event) => { event.preventDefault() const content = event.target.note.value event.target.note.value = '' props.createNote(content) } return ( <form onSubmit={addNote}> <input name="note" /> <button type="submit">add</button> </form> ) } export default connect( null, { createNote })(NewNote)
وطالما أن المكوّن لن يحتاج الوصول إلى حالة المخزن، يمكننا أن نمرر ببساطة القيمة null
كمعامل أول للدالة connect
.
يمكنك إيجاد الشيفرة الكاملة لتطبيقنا الحالي في الفرع part6-5 ضمن المستودع المخصص للتطبيق على GitHub.
الإشارة المرجعية إلى مولدات الأفعال الممررة كخصائص
لنوجه اهتمامنا إلى الميزة الهامة التي يمتلكها المكوّن NewNote
:
import React from 'react' import { connect } from 'react-redux' import { createNote } from '../reducers/noteReducer' const NewNote = (props) => { const addNote = async (event) => { event.preventDefault() const content = event.target.note.value event.target.note.value = '' props.createNote(content) } return ( <form onSubmit={addNote}> <input name="note" /> <button type="submit">add</button> </form> ) } export default connect( null, { createNote })(NewNote)
سيشعر المطورون حديثو المعرفة بالدالة connect
بالحيرة أمام نسختي مولد الأفعال createNote
في هذا المكوّن. يجب أن يُشار إلى دالة مولد الأفعال السابقة بالأمر props.createNote
من خلال خصائص المكوّن، لأنها النسخة التي تنجز الإيفاد الذي تولده الدالة connect
تلقائيًا. ونظرًا للطريقة التي يُدرج فيها مولد الفعل:
import { createNote } from './../reducers/noteReducer'
يمكن أن يُشار إلى مولد الفعل مباشرة بطلب الدالة creatNote
. لكن لا تفعل ذلك، لأنها النسخة غير المعدلة من دالة مولد الفعل وبالتالي لا تمتلك آلية الإيفاد التلقائي.
إن طبعنا الدالتين على الطرفية (لم نتطرق إلى هذه الحيلة في التنقيح بعد) كالتالي:
const NewNote = (props) => { console.log(createNote) console.log(props.createNote) const addNote = (event) => { event.preventDefault() const content = event.target.note.value event.target.note.value = '' props.createNote(content) } // ... }
يمكن أن نجد الفرق بين الدالتين كما في الشكل التالي:
حيث تظهر الدالة الأولى بأنها دالة مولد فعل نظامية، بينما ستحتوي الدالة الثانية آلية إيفاد إلى المخزن، أضافتها الدالة connect
. تمثل الدالة connect
أداة غاية في الأهمية بالرغم من أنها تبدو صعبة الفهم في البداية، نظرًا لمستوى التجريد العالي الذي تقدمه.
طرائق أخرى لاستخدام الدالة mapDispatchToProps
لقد عرفنا دالة إيفاد الأفعال من خلال المكوّن المتصل NewNote
على النحو التالي:
const NewNote = () => { // ... } export default connect( null, { createNote } )(NewNote)
تُمكننا عبارة connect
السابقة من إيفاد الأفعال بغية إنشاء ملاحظات جديدة، وذلك باستعمال الأمر ('props.createNote('a new note
. ينبغي أن تكون الدوال الممررة إلى الدالة mapDispatchToProps
مولدات أفعال، أي دوال تعيد أفعال Redux، فلا فائدة من تمرير كائن JavaScript كمعامل لهذه الدالة.
فالتعريف التالي:
{ createNote }
هو اختصار لتعريف كائن مجرّد:
{ createNote: createNote }
وهو كائن يمتلك خاصية واحدة هي createNote
تأتي مع الدالة createNote
كقيمة لها. بدلًا من ذلك، يمكن تمرير التعريف التالي لدالة كمعامل آخر للدالة connect
:
const NewNote = (props) => { // ... } const mapDispatchToProps = dispatch => { return { createNote: value => { dispatch(createNote(value)) }, }} export default connect( null, mapDispatchToProps )(NewNote)
بهذه الطريقة ستدفع الدالة connect
بالدالة mapDispatchtoProps
بتمريرها لدالة الإيفاد dispatch
كمعامل لها. وستكون النتيجة كائن يعرّف مجموعة من الدوال التي ستمرر إلى المكوّن المتصل كخصائص.
يعرّف تطبيقنا الدالة التي ستُمرر إلى connect
على أنها الخاصية createNote
:
value => { dispatch(createNote(value)) }
حيث توفد ببساطة الفعل الذي أنشأته دالة مولد الأفعال createNote
. يشير بعدها المكوِّن إلى الدالة عبر خصائصه من خلال الأمر props.creatNote
:
const NewNote = (props) => { const addNote = (event) => { event.preventDefault() const content = event.target.note.value event.target.note.value = '' props.createNote(content) } return ( <form onSubmit={addNote}> <input name="note" /> <button type="submit">add</button> </form> ) }
يحمل هذا المفهوم شيئًا من التعقيد ويصعب شرحه وتوضيحه من خلال كتابته، ويكفي في معظم الحالات استخدام الشكل الأبسط للدالة mapDispatchToProps
. لكن تظهر الحاجة في بعض الحالات إلى استخدام الشكل المعقد، كالحالة التي يتطلب فيها إيفاد الفعل إلى الإشارة المرجعية إلى خصائص الكائن.
أعدّ مصمم Redux دان آبراموف دورة تعليمية مميزة بعنوان Getting started with Redux يمكن أن تجدها على Egghead.io. ننصح الجميع بالاطلاع عليها. وستركز الفيديوهات الأربعة الأخيرة من الدورة على connect
وخاصة الطرق الأكثر تعقيدًا في استخدامها.
مرور آخر على المكونات التقديمية ومكونات الحاويات
يُركز المكون الذي أعدنا تشكيله كليًا على تصيير الملاحظات، فهو قريب جدًا مما ندعوه بالمكونات التقديمية. وبناء على الوصف الذي قدمه دان آبراموف، فالمكونات التقديمية:
- تهتم بكيفية عرض الأشياء
- يمكن أن تضم مكوّنات تقديمية أو حاويات، كما قد تحتوي على شيفرة DOM وتنسيقات CSS خاصة بها.
- غالبًا ما تسمح باحتواء المكوّنات من خلال الخصائص الأبناء pops.children.
- لا تتعلق ببقية التطبيق، كأفعال Redux أو مخازنها.
- لا تحدد الطريقة التي تُحمّل بها البيانات أو كيف تتغير.
- تستقبل البيانات والاستدعاءات من خلال الخصائص حصرًا.
- بالنادر ما تمتلك حالة مكوّن خاصة بها، وعندما تمتلك حالة فهي حالة واجهة مستخدم أكثر من كونها حالة تخزين بيانات.
- تُنشأ كمكوّنات وظيفية، إلّا عندما تحتاج إلى حالة أو خطافات دورة عمل أو استمثال أداء.
تحقق المكونات المتصلة التي أنشأتها الدالة connect
:
const mapStateToProps = (state) => { if ( state.filter === 'ALL' ) { return { notes: state.notes } } return { notes: (state.filter === 'IMPORTANT' ? state.notes.filter(note => note.important) : state.notes.filter(note => !note.important) ) } } const mapDispatchToProps = { toggleImportanceOf, } export default connect( mapStateToProps, mapDispatchToProps )(Notes)
وصف مكونات الحاويات، كما جاء في وصف دان آبراموف لمكونات الحاويات:
- تركز على كيفية عمل الأشياء
-
يمكن أن تضم مكونات تقديمية ومكونات حاويات، لكنها لا تحتوي عادة على شيفرة DOM خاصة بها ماعدا بعض عناصر
div
التي تغلف أجزاء من الشيفرة، ولا تمتلك أية تنسيقات CSS. - تزود المكوِّنات التقديمية أو مكونات الحاويات الأخرى بالبيانات أوتزودها بطريقة سلوكها.
- تطلب أفعال Redux وتزود المكونات التقديمية بها كاستدعاءات.
- غالبًا ما تحتوي على حالة، فطبيعتها تجعلها تميل إلى تزويد غيرها بالبيانات.
-
تُنشئها عادة المكونات الأعلى مثل
connect
العائدة للمكتبة React-Redux
إنّ تصنيف المكونات إلى تقديمية ومكونات حاويات هي طريقة لتنظيم هيكلية تطبيقات React. وقد يكون ذلك ميزة تصميمة جيدة وقد لا يكون، حسب الوضع المدروس.
وقد أشار آبراموف إلى الحسنات التالية لهذا التقسيم:
- فصل أفضل للمكونات. فستفهم تطبيقك وواجهة المستخدم التي صممتها بشكل أفضل على هذا النحو.
- قابلية استخدام أفضل. حيث يمكنك استخدام المكونات التقديمية مع مصادر مختلفة للحالة، وتحويل هذه المكونات إلى مكونات حاويات يمكن إعادة استخدامها من جديد.
- ستؤمن لك المكوّنات التقديمية بشكل رئيسي أدواتك الفنية. حيث يمكنك وضعهم في صفحة واحدة متيحًا للمصمم أن يغيّر ما يشاء دون المساس بمنطق التطبيق. ويمكنك ذلك من اختبار التغيرات في تصميم الصفحة.
ويشير آبراموف إلى مصطلح المكوّن من المرتبة العليا، فالمكوّن Note
هو مثال عن المكوّن النظامي، بينما تمثل الدالة connect
التي تتبع إلى المكتبة React-Redux مكوّنًا من مرتبة عليا. فالمكونات من مراتب عليا هي بشكل أساسي دوال تقبل مكونات نظامية كمعاملات لها، ومن ثم تعيد مكوّنًا نظاميًا.
تمثل المكوّنات من مراتب عليا (HOCs) طريقة في تعريف الدوال المعمّمة التي يمكن أن تُطبق على المكوّنات. وهو مفهوم يعود أصلًا إلى أسلوب البرمجة بالدوال ويقابل بشكل ما مفهوم الوراثة في البرمجة كائنية التوجه.
تعتبر في واقع الأمر المكونات من المراتب العليا تعميمًا لمفهوم الدوال من المراتب العليا (HOF). وهي دوال قد تقبل دوال أخرى كمعاملات أو أن تعيد دوال. لقد استخدمنا بشكل مستمر خلال المنهاج هذا النوع من الدوال، وكمثال عليها التوابع التي تتعامل مع المصفوفات مثل map
وfilter
وfind
.
انخفضت شعبية HOCs بعد ظهور واجهة الخطافات البرمجية في React. وعُدّلت جميع المكتبات التي اعتمدت عليها لتستخدم الخطافات. فالخطافات في أغلب الأوقات أسهل استعمالًا من HOC كما هي الحال في Redux.
المكتبة Redux وحالة المكون
لقد قطعنا شوطًا طويلًا في هذا المنهاج، ووصلنا إلى النقطة التي أصبحنا قادرين فيها على استخدام React بالطريقة الصحيحة. ويعني هذا أنّ React ستركز على توليد مظهر التطبيق، وستنفصل حالة التطبيق كليًا عن المكوّنات وتُمرر إلى Redux سواء أفعالها أو دوال الاختزال التي تستخدمها.
لكن ما هو وضع الخطاف useState
الذي يزوّد المكوّن بحالة خاصة به؟ هل له أي دور في حال استخدم التطبيق Redux أو أي حل آخر لإدارة الحالة؟ إن امتلك التطبيق نماذج أكثر تعقيدًا، فمن الأفضل أن تتمتع بطريقة ذاتية لإدارة حالتها ويكون ذلك باستخدام الدالةuseState
. وبالطبع نستطيع الاعتماد أيضًا على Redux لإدارة حالة النماذج، لكن من الأفضل ترك إدارة الحالة للمكوّن عندما تتعلق الحالة بملء حقول النموذج مثلًا.
هل ينبغي استخدام Redux دائمًا؟ ربما لا، فقد ناقش دان آبراموف ذلك في مقالته You Might Not Need Redux.
تتوفر حاليًا طرق أخرى لإدارة الحالة بطرق مشابهة للمكتبة Redux كاستخدام الواجهة البرمجية التي تؤمنها React واستخدام الخطاف useReducer. يمكن الاطلاع على طريقة عمل الواجهة وعمل الخطاف من خلال الانترنت، كما سنناقش ذلك في قسم لاحق.
التمارين 6.19 - 6.21
6.19 تطبيق الطرائف باستخدام connect: الخطوة 1
يصل المكوّن حاليًا إلى مخزن Redux عبر الخطافين useSelector
وuseDispatch
. عدّل المكوّن AnecdoteList
لكي يستخدم الدالة connect
بدلًا من الخطافات.
يمكنك استخدام الدوال mapStateToProps
وmapDispatchToProps
بما تراه مناسبًا.
6.20 تطبيق الطرائف باستخدام connect: الخطوة 2
عدّل كما فعلت سابقًا المكوّنين Filter
وAncedoteForm
.
6.211 تطبيق الطرائف: الخاتمة
ربما سنواجه ثغرة مزعجة في التطبيق. إن نقر المستخدم على زر التصويت في سطر واحد عدة مرات ستظهر رسالة التنبيه بشكل مضحك. فإن صوّت المستخدم مرتين خلال ثلاث ثوانٍ، فسيظهر التنبيه الأخير لمدة ثانيتين فقط(على فرض أنّ التنبيه يدوم 5 ثوانٍ بشكل طبيعي). ويحدث ذلك لأنّ إزالة التنبيه الأول يزيل الثاني مصادفةً.
أصلح هذه الثغرة، ليبقى التنبيه الصادر عن آخر تصويت مدة خمس ثوانٍ وذلك عند التصويت لعدة مرات. ويُنفّذ الأمر بإزالة التنبيه السابق عندما يتم عرض التنبيه التالي عندما يكون ذلك ضروريًا. يمكنك الاستفادة من توثيق الدالة setTimeout
أيضًا.
وهكذا نكون قد وصلنا إلى آخر تمارين القسم وحان الوقت لإرسال الحلول إلى GitHub والإشارة إلى أنك أكملت التمارين ضمن منظومة تسليم التمارين.
ترجمة -وبتصرف- للفصل connect من سلسلة Deep Dive Into Modern Web Development
أفضل التعليقات
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.