لنتابع عملنا على نسخة Redux المبسطة من تطبيق الملاحظات. ولكي نسهل الطريق علينا في تطوير المطلوب، سنعدّل دالة الاختزال بحيث نهيئ حالة المخزّن ليحوي ملاحظتين:
const initialState = [ { content: 'reducer defines how redux store works', important: true, id: 1, }, { content: 'state of store can contain any data', important: false, id: 2, }, ] const noteReducer = (state = initialState, action) => { // ... } // ... export default noteReducer
مخزن يضم حالة مركبة
لنضف إمكانية انتقاء الملاحظات التي ستُعرض للمستخدم. ستحتوي واجهة المستخدم أزرار انتقاء (Radio Buttons) لإنجاز المطلوب.
لنبدأ بإضافة بسيطة جدًا ومباشرة للشيفرة:
import React from 'react' import NewNote from './components/NewNote' import Notes from './components/Notes' const App = () => { const filterSelected = (value) => { console.log(value) } return ( <div> <NewNote /> <div> all <input type="radio" name="filter" onChange={() => filterSelected('ALL')} /> important <input type="radio" name="filter" onChange={() => filterSelected('IMPORTANT')} /> nonimportant <input type="radio" name="filter" onChange={() => filterSelected('NONIMPORTANT')} /> </div> <Notes /> </div> ) }
وطالما أنّ الصفة name
هي نفسها لكل أزرار الانتقاء، فستشكِّل هذه الأزرار مجموعة واحدة يمكن اختيار أحدها فقط. تمتلك الأزرار معالج تغيرات وظيفته الحالية طباعة النص الذي يظهر بجوار الزر على الطرفية.
قررنا إضافة وظيفة لانتقاء الملاحظات بتخزين قيمة مُرشّح الانتقاء (filter) في مخزن Redux إلى جانب الملاحظات. ستبدو حالة المخزن بعد إجراء التعديلات على النحو التالي:
{ notes: [ { content: 'reducer defines how redux store works', important: true, id: 1}, { content: 'state of store can contain any data', important: false, id: 2} ], filter: 'IMPORTANT' }
ستُخزَّن فقط مصفوفة الملاحظات في حالة المُرشِّح بعد إضافة الوظيفة السابقة إلى التطبيق. سيمتلك الآن كائن الحالة خاصيتين هما notes
التي تضم مصفوفة الملاحظات، وfilter
التي تضم النص الذي يشير إلى الملاحظات التي ينبغي عرضها للمستخدم.
دوال الاختزال المدمجة
يمكننا تعديل دالة الاختزال في تطبيقنا لتتعامل مع الشكل الجديد للحالة. لكن من الأفضل في وضعنا هذا تعريف دالة اختزال جديدة خاصة بحالة المرشِّح:
const filterReducer = (state = 'ALL', action) => { switch (action.type) { case 'SET_FILTER': return action.filter default: return state } }
تمثل الشيفرة التالية الأفعال التي ستغير حالة المُرشِّح:
{ type: 'SET_FILTER', filter: 'IMPORTANT' }
لننشئ أيضًا دالة توليد أفعال جديدة. وسنكتب الشيفرة اللازمة في وحدة جديدة نضعها في الملف "src/reducers/filterReducer.js":
const filterReducer = (state = 'ALL', action) => { // ... } export const filterChange = filter => { return { type: 'SET_FILTER', filter, } } export default filterReducer
يمكننا كتابة دالة الاختزال الفعلية لتطبيقنا بدمج الدالتين السابقتين باستخدام الدالة combineReducers.
لنعرِّف دالة الاختزال المدمجة في الملف "index.js":
import React from 'react' import ReactDOM from 'react-dom' import { createStore, combineReducers } from 'redux'import { Provider } from 'react-redux' import App from './App' import noteReducer from './reducers/noteReducer' import filterReducer from './reducers/filterReducer' const reducer = combineReducers({ notes: noteReducer, filter: filterReducer}) const store = createStore(reducer) console.log(store.getState()) ReactDOM.render( /* <Provider store={store}> <App /> </Provider>, */ <div />, document.getElementById('root') )
وطالما أنّ تطبيقنا سيخفق تمامًا عند بلوغنا هذه النقطة، سنصيّر عنصر div
فارغ بدلًا من المكون App
. ستُطبع الآن حالة المخزن على الطرفية:
لاحظ أنّ للمخزن الشكل الذي نريده تمامًا. لنلق نظرة أقرب على طريقة إنشاء دالة الاختزال المدمجة:
const reducer = combineReducers({ notes: noteReducer, filter: filterReducer, })
إنّ حالة المخزن التي عرفناها في الشيفرة السابقة، هي كائن له خاصيتين: notes
وfilter
. تُعرَّف قيمة الخاصية من خلال الدالة noteReducer
والتي لن تتعامل مع الخواص الأخرى للكائن، كما تتحكم الدالة filterReducer
بالخاصية الأخرى filter
فقط.
قبل أن نتابع سلسلة التغيرات التي نجريها على الشيفرة، لنلق نظرة على طريقة تغيير حالة المخزن باستخدام الأفعال التي تُعرِّفها دوال الاختزال المدمجة. لنضف الشيفرة التالية إلى الملف "index.js":
import { createNote } from './reducers/noteReducer' import { filterChange } from './reducers/filterReducer' //... store.subscribe(() => console.log(store.getState())) store.dispatch(filterChange('IMPORTANT')) store.dispatch(createNote('combineReducers forms one reducer from many simple reducers'))
بمحاكاة الطريقة التي تُنشأ بها الملاحظة الجديدة ومحاكاة التغييرات التي تطرأ على حالة المرشِّح بهذا الأسلوب، ستُطبع حالة المخزن على الطرفية في كل مرة يحدث فيها تغيير على المخزن:
ويجدر بك الآن أن تنتبه إلى تفصيل صغير لكنه هام، فلو أضفنا أمر الطباعة إلى الطرفية في بداية كلتا دالتي الاختزال:
const filterReducer = (state = 'ALL', action) => { console.log('ACTION: ', action) // ... }
فقد يُهيّأ للشخص بناءً على ما طُبع في الطرفية، أنّ كل فعل قد نُفِّذ مرتين:
هل هناك ثغرة ياترى في تطبيقنا؟ الجواب هو لا. إذ تقتضي طريقة عمل دوال الاختزال المدمجة التعامل مع كل فعل في كل جزء من الدالة. ومن المفترض أن تهتم دالة اختزال واحدة بفعل محدد، لكن قد تصادفنا حالات تغيّر فيها عدة دوال اختزال الأجزاء التي تتعامل معها من الحالة اعتمادًا على نفس الفعل.
إكمال شيفرة مُرشِّحات الانتقاء
لنكمل التطبيق بحيث يستخدم دوال الاختزال المدمجة. سنبدأ بتغيير طريقة تصيير التطبيق، ولنعلّق المخزن ضمنه وذلك بتعديل الملف "index.js"
ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') )
سنصلح تاليًا الثغرة التي نتجت عن توقع الشيفرة بأن تكون حالة التطبيق عبارة عن مصفوفة من الملاحظات:
يمكن إنجاز ذلك بسهولة، لأنّ الملاحظات مخزنة أصلًا في الحقل notes
من المخزن. لذلك سيفي التعديل البسيط التالي على دالة الانتقاء بالغرض:
const Notes = () => { const dispatch = useDispatch() const notes = useSelector(state => state.notes) return( <ul> {notes.map(note => <Note key={note.id} note={note} handleClick={() => dispatch(toggleImportanceOf(note.id)) } /> )} </ul> ) }
أعادت دالة الانتقاء سابقًا حالة المخزن بأكملها:
const notes = useSelector(state => state)
بينما ستعيد الآن الحقل notes
فقط:
const notes = useSelector(state => state.notes)
لنفصل مرشّح إظهار الملاحظات في مكوِّن خاص به ضمن الملف "src/components/VisibilityFilter.js":
import React from 'react' import { filterChange } from '../reducers/filterReducer' import { useDispatch } from 'react-redux' const VisibilityFilter = (props) => { const dispatch = useDispatch() return ( <div> all <input type="radio" name="filter" onChange={() => dispatch(filterChange('ALL'))} /> important <input type="radio" name="filter" onChange={() => dispatch(filterChange('IMPORTANT'))} /> nonimportant <input type="radio" name="filter" onChange={() => dispatch(filterChange('NONIMPORTANT'))} /> </div> ) } export default VisibilityFilter
سيصبح المكوِّن App
بعد إنشاء المكوِّن السابق بسيطًا كالتالي:
import React from 'react' import Notes from './components/Notes' import NewNote from './components/NewNote' import VisibilityFilter from './components/VisibilityFilter' const App = () => { return ( <div> <NewNote /> <VisibilityFilter /> <Notes /> </div> ) } export default App
ستظهر نتيجة الإضافة مباشرة الآن. فبالنقر على أزرار الانتقاء ستتغير حالة الخاصية filter
للمخزن. لنغيّر المكوِّن Notes
بحيث يتضمن المرشِّح:
const Notes = () => { const dispatch = useDispatch() const notes = useSelector(state => { if ( state.filter === 'ALL' ) { return state.notes } return state.filter === 'IMPORTANT' ? state.notes.filter(note => note.important) : state.notes.filter(note => !note.important) }) return( <ul> {notes.map(note => <Note key={note.id} note={note} handleClick={() => dispatch(toggleImportanceOf(note.id)) } /> )} </ul> )
نجري التعديلات على دالة الانتقاء فقط، والتي كانت:
useSelector(state => state.notes)
لنبسّط دالة الانتقاء بتفكيك حقل الحالة الذي يُمرَّر إليها كمعامل:
const notes = useSelector(({ filter, notes }) => { if ( filter === 'ALL' ) { return notes } return filter === 'IMPORTANT' ? notes.filter(note => note.important) : notes.filter(note => !note.important) })
لكن هناك عيب بسيط في التطبيق. فعلى الرغم من ضبط المرشِّح افتراضيًا على القيمة ALL
، لن يُختار أي زر من أزراء الانتقاء. يمكننا إيجاد الحل طبعًا، لكننا سنؤجل ذلك إلى وقت لاحق، طالما أنّ الثغرة لن تتسبب بأية مشاكل سوى أنها غير مرغوبة.
أدوات تطوير Redux
يمكن تثبيت الموسِّع Redux DevTools على المتصفح، الذي يساعد على مراقبة حالة مخزن والأفعال التي تغيّرها من خلال طرفية المتصفح. بالإضافة إلى الموسِّع السابق، يمكننا الاستفادة من المكتبة redux-devtools-extension أثناء التنقيح. لنثبت هذه المكتبة كالتالي:
npm install --save-dev redux-devtools-extension
سنجري تعديلًا بسيطًا على تعريف المخزن لنتمكن من العمل مع المكتبة:
// ... import { createStore, combineReducers } from 'redux' import { composeWithDevTools } from 'redux-devtools-extension' import noteReducer from './reducers/noteReducer' import filterReducer from './reducers/filterReducer' const reducer = combineReducers({ notes: noteReducer, filter: filterReducer }) const store = createStore( reducer, composeWithDevTools()) export default store
عندما نفتح الطرفية الآن، ستبدو نافذة Redux كالتالي:
يمكن بسهولة مراقبة كل فعل يجري على المخزن كما يُظهر الشكل التالي:
كما يمكن إيفاد الأفعال إلى المخزن مستخدمين الطرفية كالتالي:
يمكنك إيجاد الشيفرة الكاملة لتطبيقنا الحالي في الفرع part6-2 من المستودع المخصص للتطبيق على GitHub.
التمارين 6.9 - 6.12
لنكمل العمل على تطبيق الطرائف باستخدام Redux والذي بدأناه في الفصل السابق.
6.9 تطبيق طرائف أفضل: الخطوة 7
ابدأ باستخدام Redux DevTools. وانقل تعريف مخزن Redux إلى ملف خاص به وسمّه "store.js"
6.10 تطبيق طرائف أفضل: الخطوة 8
يمتلك التطبيق جسمًا معدًا مسبقًا يضم المكوَّن Notification
:
import React from 'react' const Notification = () => { const style = { border: 'solid', padding: 10, borderWidth: 1 } return ( <div style={style}> render here notification... </div> ) } export default Notification
وسّع المكوِّن ليصيّر الرسالة المخزنّة في مخزن Redux، وليأخذ الشكل التالي:
import React from 'react' import { useSelector } from 'react-redux' const Notification = () => { const notification = useSelector(/* something here */) const style = { border: 'solid', padding: 10, borderWidth: 1 } return ( <div style={style}> {notification} </div> ) }
عليك أن تجري تعديلات على دالة الاختزال الموجودة في التطبيق. أنشئ دالة اختزال جديدة للوظيفة الجديدة، ثم أعد كتابة التطبيق ليستخدم دالة اختزال مدمجة كما فعلنا سابقًا في هذا الفصل.
لا تستخدم المكوّن Notification
لغير الغرض الذي أنشئ لأجله حاليًا. يكفي أن يُظهر التطبيق القيمة الأولية للرسالة في الدالة notificationReducer
.
6.11 تطبيق طرائف أفضل: الخطوة 9
وسّع التطبيق لكي يعرض المكون الرسالة مدة 5 ثوانٍ، وذلك عندما يصوّت المستخدم لصالح طرفة أو عندما ينشئ طرفة جديدة.
ننصحك بإنشاء دالة مولد أفعال منفصلة من أجل ضبط و حذف التنبيهات.
6.12 تطبيق طرائف أفضل: الخطوة 10 *
أضف وظيفة لانتقاء الطرائف التي ستعرض للمستخدم.
خزّن حالة المرشِّح في مخزن Redux. ويفضّل إنشاء دالة اختزال ومولد أفعال جديدين لهذه الغاية. أنشئ المكوّن الجديد Filter
لعرض الطرائف التي تم انتقاؤها. يمكنك استخدام الشيفرة التالية كقالب للمكوِّن:
import React from 'react' const Filter = () => { const handleChange = (event) => { // input-field value is in variable event.target.value } const style = { marginBottom: 10 } return ( <div style={style}> filter <input onChange={handleChange} /> </div> ) } export default Filter
ترجمة -وبتصرف- للفصل Many reducers من سلسلة Deep Dive Into Modern Web Development
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.