اذهب إلى المحتوى

فهم دوال الاختزال المتعددة (reducers) في مكتبة Redux في تطبيقات React


ابراهيم الخضور

لنتابع عملنا على نسخة 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) لإنجاز المطلوب.

note_filter_ui_01.png

لنبدأ بإضافة بسيطة جدًا ومباشرة للشيفرة:

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. ستُطبع الآن حالة المخزن على الطرفية:

store_state_comReducers_02.png

لاحظ أنّ للمخزن الشكل الذي نريده تمامًا. لنلق نظرة أقرب على طريقة إنشاء دالة الاختزال المدمجة:

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'))

بمحاكاة الطريقة التي تُنشأ بها الملاحظة الجديدة ومحاكاة التغييرات التي تطرأ على حالة المرشِّح بهذا الأسلوب، ستُطبع حالة المخزن على الطرفية في كل مرة يحدث فيها تغيير على المخزن:

state_changes_logged_03.png

ويجدر بك الآن أن تنتبه إلى تفصيل صغير لكنه هام، فلو أضفنا أمر الطباعة إلى الطرفية في بداية كلتا دالتي الاختزال:

const filterReducer = (state = 'ALL', action) => {
  console.log('ACTION: ', action)
  // ...
}

فقد يُهيّأ للشخص بناءً على ما طُبع في الطرفية، أنّ كل فعل قد نُفِّذ مرتين:

action_seems_dupicated_04.png

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

إكمال شيفرة مُرشِّحات الانتقاء

لنكمل التطبيق بحيث يستخدم دوال الاختزال المدمجة. سنبدأ بتغيير طريقة تصيير التطبيق، ولنعلّق المخزن ضمنه وذلك بتعديل الملف "index.js"

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

سنصلح تاليًا الثغرة التي نتجت عن توقع الشيفرة بأن تكون حالة التطبيق عبارة عن مصفوفة من الملاحظات:

state_bug_fixed_05.png

يمكن إنجاز ذلك بسهولة، لأنّ الملاحظات مخزنة أصلًا في الحقل 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 كالتالي:

console_redux_tab_06.png

يمكن بسهولة مراقبة كل فعل يجري على المخزن كما يُظهر الشكل التالي:

console_actions_monitoring_07.png

كما يمكن إيفاد الأفعال إلى المخزن مستخدمين الطرفية كالتالي:

console_action_dispatch_08.png

يمكنك إيجاد الشيفرة الكاملة لتطبيقنا الحالي في الفرع 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 ثوانٍ، وذلك عندما يصوّت المستخدم لصالح طرفة أو عندما ينشئ طرفة جديدة.

anecnode_vote_message_09.png

ننصحك بإنشاء دالة مولد أفعال منفصلة من أجل ضبط و حذف التنبيهات.

6.12 تطبيق طرائف أفضل: الخطوة 10 *

أضف وظيفة لانتقاء الطرائف التي ستعرض للمستخدم.

anecnodes_filter_10..png

خزّن حالة المرشِّح في مخزن 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


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...