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

استعمال المعمارية Flux والمكتبة Redux لإدارة الحالة في تطبيقات React


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

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

المعمارية Flux

طورت Facebook المعمارية Flux لتسهيل إدارة الحالة في التطبيقات. حيث تُفصل الحالة في هذه المعمارية بشكل كامل عن المكوِّنات ضمن مخازنها الخاصة. لا تتغير الحالة الموجودة في المخازن مباشرة، بل عبر العديد من الأفعال. فعندما يغير الفعل حالة المخزن، يُعاد تصيير المشهد كاملًا:

store_state_changed_01.png

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

store_changed_by_action_02.png

تقدم Flux طريقة معيارية عن كيفية ومكان حفظ الحالة وعن كيفية تعديلها.

المكتبة Redux

قدّمت FaceBook طريقة لاستخدام معمارية Flux، لكننا سنستخدم المكتبة Redux. تعمل هذه المكتبة وفق المبدأ نفسه، لكنها أسهل قليلًا. كما أنّ Facebook نفسها بدأت باستخدام Redux بدلًا من الطريقة التي ابتكرتها. سنتعرف على أسلوب عمل Redux من خلال إنشاء تطبيق العداد مرة أخرى:

Redux_imp_03.png

أنشئ تطبيقًا باستخدام createreactapp وثبِّت Redux كالتالي: Redux

npm install redux

تُخزّن الحالة في Redux في المخازن، كما هو الحال في Flux.

تُخزَّن حالة التطبيق بأكملها ضمن كائن JavaScript واحد ويوضع في مخزن. لكن طالما أن تطبيقنا سيحتاج إلى قيمة العداد فقط، سنحفظه مباشرة في المخزن (دون الحاجة لوضعه في كائن JavaScript أولًا). بينما لو كانت الحالة أكثر تعقيدًا، لابدّ من حفظ كل جزء ضمن حقل منفصل من حقول كائن JavaScript.

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

{
  type: 'INCREMENT'
}

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

يُحدَّد تأثير الفعل على حالة التطبيق باستخدام دوال الاختزال reducer. وهي في الواقع دوال يُمرر إليها كل من الحالة في وضعها الراهن والفعل كمعاملين. تعيد هذه الدوال الحالة الجديدة.

لنعرّف الآن دالة اختزال في تطبيقنا:

const counterReducer = (state, action) => {
  if (action.type === 'INCREMENT') {
    return state + 1
  } else if (action.type === 'DECREMENT') {
    return state - 1
  } else if (action.type === 'ZERO') {
    return 0
  }

  return state
}

نلاحظ أن الوسيط الأول هو الحالة والثاني هو الفعل. تُعيد دالة الاختزال حالة جديدة وفقًا لنوع الفعل. لنغيّر الشيفرة قليلًا. فمن العادة استخدام بنية التعداد switch بدلًا من البنية الشرطية if في دوال الاختزال. ولنجعل أيضًا القيمة 0 قيمة افتراضية للحالة. وهكذا ستعمل دالة الاختزال حتى لو لم تُحَّدد الحالة بعد:

const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    case 'ZERO':
      return 0
    default: // إن لم نحصل على إحدى الحالات السابقة 
    return state
  }
}

من غير المفترض أن تُستدعى دالة الاختزال مباشرة من خلال شيفرة التطبيق. حيث تُمرَّر هذه الدوال كوسطاء إلى الدالة createStore التي تُنشئ مخزنًا:

import { createStore } from 'redux'

const counterReducer = (state = 0, action) => {
  // ...
}

const store = createStore(counterReducer)

سيستخدم المخزن الآن دالة الاختزال للتعامل مع الأفعال التي تُرسل إلى المخزن من خلال التابع dispatch.

store.dispatch({type: 'INCREMENT'})

كما يمكن إيجاد حالة مخزن باستخدام الأمر getState. ستطبع على سبيل المثال الشيفرة التالية:

const store = createStore(counterReducer)
console.log(store.getState())
store.dispatch({type: 'INCREMENT'})
store.dispatch({type: 'INCREMENT'})
store.dispatch({type: 'INCREMENT'})
console.log(store.getState())
store.dispatch({type: 'ZERO'})
store.dispatch({type: 'DECREMENT'})
console.log(store.getState())

المعلومات التالية على الطرفية:

0
3
-1

حيث كانت القيمة الافتراضية للحالة هي 0. لكن بعد ثلاثة أفعال زيادة INCREMENT، أصبحت قيمة الحالة 3. وأخيرُا بعد تنفيذ فعل التصفير ZERO وفعل الإنقاص DECREMENT أصبحت قيمة الحالة -1.

يستخدم التابع subscribe الذي يعود لكائن المخزن في إنشاء دوال الاستدعاء التي يستخدمها المخزن عندما تتغير حالته. فلو أضفنا على سبيل المثال الدالة التالية إلى التابع subscribe، فسيُطبع كل تغيير في الحالة على الطرفية:

store.subscribe(() => {
  const storeNow = store.getState()
  console.log(storeNow)
})

وهكذا سيؤدي تنفيذ الشيفرة التالية:

const store = createStore(counterReducer)

store.subscribe(() => {
  const storeNow = store.getState()
  console.log(storeNow)
})

store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'ZERO' })
store.dispatch({ type: 'DECREMENT' })

إلى طباعة ما يلي على الطرفية:

1
2
3
0
-1

تمثل الشيفرة التالية شيفرة تطبيق العداد وقد كتبت جميعها في نفس الملف. لاحظ أننا استخدمنا المخازن بشكل مباشر ضمن شيفرة React. سنتعلم لاحقًا طرقًا أفضل لبناء شيفرة React/Redux

import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'

const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    case 'ZERO':
      return 0
    default:
      return state
  }
}

const store = createStore(counterReducer)

const App = () => {
  return (
    <div>
      <div>
        {store.getState()}
      </div>
      <button 
        onClick={e => store.dispatch({ type: 'INCREMENT' })}
      >
        plus
      </button>
      <button
        onClick={e => store.dispatch({ type: 'DECREMENT' })}
      >
        minus
      </button>
      <button 
        onClick={e => store.dispatch({ type: 'ZERO' })}
      >
        zero
      </button>
    </div>
  )
}

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

renderApp()
store.subscribe(renderApp)

ستجد عدة نقاط ملفتة في الشيفرة السابقة. سيصير المكوِّن App قيمة العداد بالحصول على قيمته من المخزن باستخدام التابع ()store.getState. كما ستوفد معالجات الأفعال المعرّفة في الأزرار الأفعال المناسبة إلى المخزن.

لن تتمكن React من تصيير التطبيق تلقائيًا عندما تتغير حالة المخزن، ولهذا فقد هيئنا الدالة renderApp التي تصيّر التطبيق بأكمله لكي ترصد التغييرات في المخزن عن طريق وضعها ضمن التابع store.subscribe.

لاحظ أنه علينا استدعاء التابع renderApp مباشرة، وبدونه لن يُصيَّر المكوِّن App للمرة الأولى.

استخدام Redux مع تطبيق الملاحظات

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

const noteReducer = (state = [], action) => {
  if (action.type === 'NEW_NOTE') {
    state.push(action.data)
    return state
  }

  return state
}

const store = createStore(noteReducer)

store.dispatch({
  type: 'NEW_NOTE',
  data: {
    content: 'the app state is in redux store',
    important: true,
    id: 1
  }
})

store.dispatch({
  type: 'NEW_NOTE',
  data: {
    content: 'state changes are made with actions',
    important: false,
    id: 2
  }
})

const App = () => {
  return(
    <div>
      <ul>
        {store.getState().map(note=>
          <li key={note.id}>
            {note.content} <strong>{note.important ? 'important' : ''}</strong>
          </li>
        )}
        </ul>
    </div>
  )
}

لا يمتلك التطبيق بشكله الحالي وظيفة إضافة ملاحظات جديدة، على الرغم من إمكانية إضافتها من خلال إيفاد الفعل NEW_NOTE إلى المخزن.

لاحظ امتلاك الأفعال الآن حقلًا للنوع وآخر للبيانات ويحتوي على الملاحظة الجديدة التي سنضيفها:

{
  type: 'NEW_NOTE',
  data: {
    content: 'state changes are made with actions',
    important: false,
    id: 2
  }
}

الدوال النقيّة والدوال الثابتة

للنسخة الأولية من دالة الاختزال الشكل البسيط التالي:

const noteReducer = (state = [], action) => {
  if (action.type === 'NEW_NOTE') {
    state.push(action.data)
    return state
  }

  return state
}

تتخذ الحالة الآن شكل مصفوفة. وتسبب الأفعال من النوع NEW_NOTE إضافة ملاحظة جديدة إلى الحالة عبر التابع push.

سيعمل التطبيق، لكن دالة الاختزال قد عُرِّفت بطريقة سيئة لأنها ستخرق الافتراض الرئيسي لدوال الاختزال والذي ينص على أن دوال الاختزال يجب أن تكون دوال نقيّة pure functions.

الدوال النقية هي دوال لا تسبب أية تأثيرات جانبية وعليها أن تعيد نفس الاستجابة عندما تُستدعى بنفس المعاملات.

أضفنا ملاحظة جديدة إلى الحالة مستخدمين التابع (state.push(action.data الذي يغير وضع كائن الحالة. لكن هذا الأمر غير مسموح.

يمكن حل المشكلة بسهولة باستخدام التابع concat الذي ينشئ مصفوفة جديدة تحتوي كل عناصر المصفوفة القديمة بالإضافة إلى العنصر الجديد:

const noteReducer = (state = [], action) => {
  if (action.type === 'NEW_NOTE') {
    return state.concat(action.data)
  }

  return state
}

كما ينبغي أن تتكون دوال الاختزال من كائنات ثابتة (immutable). فلو حدث تغيّر في الحالة، فلن يغير ذلك كائن الحالة، بل سيُستبدل بكائن جديد يحتوي الحالة الجديدة. وهذا ما فعلناه تمامًا بدالة الاختزال الجديدة، حين استبدلنا المصفوفة القديمة بالجديدة.

لنوسّع دالة الاختزال لتتمكن من التعامل مع تغيّر أهمية الملاحظة:

{
  type: 'TOGGLE_IMPORTANCE',
  data: {
    id: 2
  }
}

وطالما أننا لم نكتب أية شيفرة للاستفادة من هذه الوظيفة، فما فعلناه هو توسيع دالة الاختزال بالأسلوب "المقاد بالاختبار-Test Driven". لنبدأ إذًا بكتابة اختبار يتعامل مع الفعل NEW_NOTE. ولتسهيل الاختبار، سننقل شيفرة دالة الاختزال إلى وحدة مستقلة ضمن الملف "src/reducers/noteReducer.js". سنضيف أيضًا المكتبة deep-freeze والتي تستخدم للتأكد من تعريف دالة الاختزال كدالة ثابتة. سنثبت المكتبة كاعتمادية تطوير كالتالي:

npm install --save-dev deep-freeze

تمثل الشيفرة التالية شيفرة الاختبار الموجود في الملف src/reducers/noteReducer.test.js:

import noteReducer from './noteReducer'
import deepFreeze from 'deep-freeze'

describe('noteReducer', () => {
  test('returns new state with action NEW_NOTE', () => {
    const state = []
    const action = {
      type: 'NEW_NOTE',
      data: {
        content: 'the app state is in redux store',
        important: true,
        id: 1
      }
    }

    deepFreeze(state)
    const newState = noteReducer(state, action)

    expect(newState).toHaveLength(1)
    expect(newState).toContainEqual(action.data)
  })
})

يتوثّق الأمر (deepFreeze(state من أن دالة الاختبار لن تغير حالة المخزن التي تُمرّر إليها كمعامل. وإن استخدمت دالة الاختزال التابع push لتغيير الحالة، سيخفق الاختبار.

reducer_deepfreez_04.png

سنكتب الآن اختبارًا للفعل TOGGLE_IMPORTANT:

test('returns new state with action TOGGLE_IMPORTANCE', () => {
  const state = [
    {
      content: 'the app state is in redux store',
      important: true,
      id: 1
    },
    {
      content: 'state changes are made with actions',
      important: false,
      id: 2
    }]

  const action = {
    type: 'TOGGLE_IMPORTANCE',
    data: {
      id: 2
    }
  }

  deepFreeze(state)
  const newState = noteReducer(state, action)

  expect(newState).toHaveLength(2)

  expect(newState).toContainEqual(state[0])

  expect(newState).toContainEqual({
    content: 'state changes are made with actions',
    important: true,
    id: 2
  })
})

سيغيّر الفعل التالي:

{
  type: 'TOGGLE_IMPORTANCE',
  data: {
    id: 2
  }
}

أهمية الملاحظة التي تحمل المعرّف الفريد 2.

سنوسع التطبيق كالتالي:

const noteReducer = (state = [], action) => {
  switch(action.type) {
    case 'NEW_NOTE':
      return state.concat(action.data)
    case 'TOGGLE_IMPORTANCE': {
      const id = action.data.id
      const noteToChange = state.find(n => n.id === id)
      const changedNote = { 
        ...noteToChange, 
        important: !noteToChange.important 
      }
      return state.map(note =>
        note.id !== id ? note : changedNote 
      )
     }
    default:
      return state
  }
}

أنشأنا نسخة من الملاحظة التي غيّرنا أهميتها بالطريقة التي اتبعناها في القسم 2، كما استبدلنا الحالة بأخرى جديدة تضم الملاحظات القديمة التي لم تتغير أهميتها ونسخة عن الملاحظة التي تغيرت أهميتها changedNote. لنلخص ما يجري عند تنفيذ الشيفرة. ففي البداية سنبحث عن كائن الملاحظة التي نريد أن نغير أهميتها:

const noteToChange = state.find(n => n.id === id)

سننشئ بعد ذلك كائنًا جديدًا يمثل نسخة عن الملاحظة الأصلية، لكن أهميتها قد تغيرت إلى الحالة المعاكسة:

const changedNote = { 
  ...noteToChange, 
  important: !noteToChange.important 
}

تُعاد بعد ذلك الحالة الجديدة. وذلك بأخذ جميع الملاحظات القديمة التي لم تتغير حالتها واستبدال الملاحظة التي تغيرت بالنسخة المعدلّة عنها:

state.map(note =>
  note.id !== id ? note : changedNote 
)

العبارات البرمجية لنشر مصفوفة

نمتلك الآن اختبارين جيدين لدوال الاختزال، لذلك يمكننا إعادة كتابة الشيفرة لإضافة ملاحظة جديدة تنشئ حالة تعيدها دالة المصفوفات concat. لنلق نظرة على كيفية إنجاز المطلوب باستخدام عامل نشر المصفوفة:

const noteReducer = (state = [], action) => {
  switch(action.type) {
    case 'NEW_NOTE':
      return [...state, action.data]
    case 'TOGGLE_IMPORTANCE':
      // ...
    default:
    return state
  }
}

ستعمل دالة نشر المصفوفة على النحو التالي: لو عرفنا المتغير number كالتالي:

const numbers = [1, 2, 3]

سيفصل الأمر number... المصفوفة إلى عناصرها المفردة بحيث يمكن وضع هذه العناصر في مصفوفة أخرى كالتالي:

[...numbers, 4, 5]

وستكون النتيجة [1,2,3,4,5].

لكن لو استخدمنا المصفوفة دون نشر كالتالي:

[numbers, 4, 5]

ستكون النتيجة [4,5,[1,2,3]].

كما ستبدو الشيفرة مشابهة لما سبق، عندما نفكك المصفوفة إلى عناصرها باستخدام التابع destructuring.

const numbers = [1, 2, 3, 4, 5, 6]

const [first, second, ...rest] = numbers

console.log(first)     // prints 1
console.log(second)   // prints 2
console.log(rest)     // prints [3, 4, 5, 6]

التمرينات 6.1 - 6.2

لنكتب نسخة بسيطة عن تطبيق unicafe الذي أنشأناه في القسم 1، بحيث ندير الحالة باستخدام Redux.

يمكنك الحصول على المشروع من المستودع الخاص بالتطبيق على GitHub لتستعمله كأساس لمشروعك.

إبدأ عملك بإزالة تهيئة git للنسخة التي لديك، ثم تثبيت الاعتماديات اللازمة:

cd unicafe-redux   // تنقلك هذه التعليمة إلى مجلد المستودع الذي يضمن نسختك
rm -rf .git
npm install

6.1 unicafe مرور ثانٍ: الخطوة 1

قبل أن نضيف الوظائف إلى واجهة المستخدم، سنعمل على إضافة وظيفة مستودع الحالة. إذ علينا أن نخزّن عدد الآراء من كل نوع وسيبدو شكل المخزن كالتالي:

{
  good: 5,
  ok: 4,
  bad: 2
}

سيكون لدالة الاختزال الأساسية الشكل التالي:

const initialState = {
  good: 0,
  ok: 0,
  bad: 0
}

const counterReducer = (state = initialState, action) => {
  console.log(action)
  switch (action.type) {
    case 'GOOD':
      return state
    case 'OK':
      return state
    case 'BAD':
      return state
    case 'ZERO':
      return state
  }
  return state
}

export default counterReducer

وتمثل الشيفرة التالية أساسًا للاختبارات:

import deepFreeze from 'deep-freeze'
import counterReducer from './reducer'

describe('unicafe reducer', () => {
  const initialState = {
    good: 0,
    ok: 0,
    bad: 0
  }

  test('should return a proper initial state when called with undefined state', () => {
    const state = {}
    const action = {
      type: 'DO_NOTHING'
    }

    const newState = counterReducer(undefined, action)
    expect(newState).toEqual(initialState)
  })

  test('good is incremented', () => {
    const action = {
      type: 'GOOD'
    }
    const state = initialState

    deepFreeze(state)
    const newState = counterReducer(state, action)
    expect(newState).toEqual({
      good: 1,
      ok: 0,
      bad: 0
    })
  })
})

إضافة الاختبارات ودالة الاختزال: تأكد من خلال الاختبارات أن دالة الاختزال ثابتة، مستخدمًا المكتبة deep-freeze. وتأكد من نجاح الاختبار الأول، إذ ستتوقع Redux أن تعيد دالة الاختزال قيمة منطقية للحالة الأصلية عند استدعائها، ذلك أن المعامل state الذي يمثل الحالة السابقة، لن يكون معرّفًا في البداية.

وسّع بعد ذلك دالة الاختزال بحيث ينجح الاختباران، ثم أضف بقية الاختبارات. وأخيرًا أضف الوظائف التي سُتختبر. يمكنك الاستعانة بنموذج دالة الاختزال الذي اعتمدناه في تطبيق الملاحظات السابق.

6.2 unicafe مرور ثانٍ: الخطوة 2

أضف الآن الوظائف الفعلية للتطبيق.

النماذج الحرّة

سنضيف تاليًا وظيفةً لإنشاء ملاحظة جديدة وتغيير أهميتها:

const generateId = () =>
  Number((Math.random() * 1000000).toFixed(0))

const App = () => {
  const addNote = (event) => {
    event.preventDefault()
    const content = event.target.note.value
    event.target.note.value = ''
    store.dispatch({
      type: 'NEW_NOTE',
      data: {
        content,
        important: false,
        id: generateId()
      }
    })
  }

  const toggleImportance = (id) => {
    store.dispatch({
      type: 'TOGGLE_IMPORTANCE',
      data: { id }
    })
  }

  return (
    <div>
      <form onSubmit={addNote}>
        <input name="note" /> 
        <button type="submit">add</button>
      </form>
      <ul>
        {store.getState().map(note =>
          <li
            key={note.id} 
            onClick={() => toggleImportance(note.id)}
          >
            {note.content} <strong>{note.important ? 'important' : ''}</strong>
          </li>
        )}
      </ul>
    </div>
  )
}

ستجري إضافة الوظيفتين بشكل مباشر. وانتبه إلى أننا لم نربط حالة حقول النموذج بحالة المكوّن App كما فعلنا سابقًا، ويعرف هذا النوع من النماذج في React بالنماذج الحرة (uncontrolled forms).

اقتباس

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

يمكنك الاطلاع على المزيد حول النماذج الحرة من خلال الانترنت.

تجري إضافة ملاحظة جديدة بإسلوب بسيط، حيث تضاف الملاحظة الجديدة حالما يُوفد الفعل:

addNote = (event) => {
  event.preventDefault()
  const content = event.target.note.value  event.target.note.value = ''
  store.dispatch({
    type: 'NEW_NOTE',
    data: {
      content,
      important: false,
      id: generateId()
    }
  })
}

كما يمكن الحصول على محتوى الملاحظة الجديدة من الحقل المخصص لها في النموذج مباشرةً. ذلك أننا نستطيع الوصول إلى محتويات الحقل من خلال كائن الحدث باستخدام الأمر event.target.note.value، لأن لهذا الحقل اسمًا محددًا.

<form onSubmit={addNote}>
  <input name="note" />  
  <button type="submit">add</button>
</form>

يمكن تغيير أهمية الملاحظة بالنقر على اسمها، وسيكون لمعالج الحدث الشكل البسيط التالي:

toggleImportance = (id) => {
  store.dispatch({
    type: 'TOGGLE_IMPORTANCE',
    data: { id }
  })
}

موّلدات الأفعال

لقد بدأنا نرى أهمية Redux في تبسيط شيفرة الواجهة الأمامية حتى في التطبيقات البسيطة التي أنشأناها، لكن يمكننا أيضًا فعل الكثير. لا حاجة في الواقع أن يعرف مكوّن React أنواع أفعال Redux وأشكالها. سنفصل تاليًا توليد الأفعال إلى دوال خاصة بكل فعل:

const createNote = (content) => {
  return {
    type: 'NEW_NOTE',
    data: {
      content,
      important: false,
      id: generateId()
    }
  }
}

const toggleImportanceOf = (id) => {
  return {
    type: 'TOGGLE_IMPORTANCE',
    data: { id }
  }
}

تُدعى الدوال التي تُنشئ الأفعال، مولدات الأفعال. ليس على المكوِّن App أن يعرف أي شيء عن طريقة كتابة الفعل، وكل ما يحتاجه للحصول على الفعل الصحيح هو استدعاء مولد الأفعال.

const App = () => {
  const addNote = (event) => {
    event.preventDefault()
    const content = event.target.note.value
    event.target.note.value = ''
    store.dispatch(createNote(content))    
  }

  const toggleImportance = (id) => {
    store.dispatch(toggleImportanceOf(id))
  }

  // ...
}

استخدام مخزن Redux ضمن المكونات المختلفة

باستثناء دالة الاختزال، فقد كُتب تطبيقنا في ملف واحد. وهذا أمر غير منطقي، لذا علينا فصله إلى وحدات مستقلة. لكن السؤال المطروح حاليًا: كيف يمكن للمكوِّن APP أن يصل إلى مخزن الحالة بعد عملية الفصل؟ وبشكل أعم، لا بد من وجود طريقة لوصول كل المكوِّنات الجديدة الناتجة عن تقسيم المكوّن الأصلي إلى مخزن الحالة. هنالك أكثر من طريقة لمشاركة مخزن Redux بين المكوِّنات. سنلقي نظرة أولًا على أسهل طريقة ممكنة وهي استخدام واجهة الخطافات البرمجية للمكتبة react-redux. لنثبت إذًا هذه المكتبة:

npm install react-redux

سننقل الآن المكوِّن App إلى وحدة خاصة به يمثلها الملف "App.js"، ولنرى كيف سيؤثر ذلك على بقية ملفات التطبيق. سيصبح الملف "Index.js" على النحو:

import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'import App from './App'
import noteReducer from './reducers/noteReducer'

const store = createStore(noteReducer)

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

لاحظ كيف عُرِّف التطبيق الآن كابن لمكوّن التزويد Provider الذي تقدمه المكتبة React-Redux. وكيف مُرِّر مخزن حالة التطبيق إلى مكوّن التزويد عبر الصفة store. نقلنا تعريف مولدات الأفعال إلى الملف "reducers/noteReducer.js" حيث عرفنا سابقًا دالة الاختزال. سيبدو شكل الملف كالتالي:

const noteReducer = (state = [], action) => {
  // ...
}

const generateId = () =>
  Number((Math.random() * 1000000).toFixed(0))

export const createNote = (content) => {  
    return {
    type: 'NEW_NOTE',
    data: {
      content,
      important: false,
      id: generateId()
    }
  }
}

export const toggleImportanceOf = (id) => {  
    return {
    type: 'TOGGLE_IMPORTANCE',
    data: { id }
  }
}

export default noteReducer

إن احتوى التطبيق على عدة مكوّنات يحتاج كل منها إلى الوصول إلى مخزن الحالة، يجب على المكوِّن Appتمرير المخزن store إليها كخاصية. ستحتوي الوحدة الآن على عدة أوامر تصدير، سيعيد أمر التصدير الافتراضي دالة الاختزال، لذلك يمكن إدراجها بالطريقة الاعتيادية:

import noteReducer from './reducers/noteReducer'

يمكن للوحدة أن تجري عملية تصدير واحدة بشكل افتراضي، وعدة عمليات تصدير اعتيادية.

export const createNote = (content) => {
  // ...
}

export const toggleImportanceOf = (id) => { 
  // ...
}

يمكن إدراج الدوال التي صُدرت بعملية تصدير اعتيادية بوضعها بين قوسين معقوصين:

import { createNote } from './../reducers/noteReducer'

ستصبح شيفرة المكوّن App كالتالي:

import React from 'react'
import { 
  createNote, toggleImportanceOf
} from './reducers/noteReducer' 
import { useSelector, useDispatch } from 'react-redux'

const App = () => {
  const dispatch = useDispatch()  
  const notes = useSelector(state => state)
  const addNote = (event) => {
    event.preventDefault()
    const content = event.target.note.value
    event.target.note.value = ''
    dispatch(createNote(content))  
  }

  const toggleImportance = (id) => {
    dispatch(toggleImportanceOf(id))  
  }

  return (
    <div>
      <form onSubmit={addNote}>
        <input name="note" /> 
        <button type="submit">add</button>
      </form>
      <ul>
        {notes.map(note =>          
         <li
            key={note.id} 
            onClick={() => toggleImportance(note.id)}
          >
            {note.content} <strong>{note.important ? 'important' : ''}</strong>
          </li>
        )}
      </ul>
    </div>
  )
}

export default App

تجدر ملاحظة عدة نقاط في الشيفرة السابقة. فقد جرى سابقًا إيفاد الأفعال باستخدام التابع dispatch العائد لمخزن Redux:

store.dispatch({
  type: 'TOGGLE_IMPORTANCE',
  data: { id }
})

أمّا الآن فيجري باستخدام الدالة dispatch العائدة للخطاف useDispatch:

import { useSelector, useDispatch } from 'react-redux'
const App = () => {
  const dispatch = useDispatch()  // ...

  const toggleImportance = (id) => {
    dispatch(toggleImportanceOf(id))  }

  // ...
}

يؤمن الخطاف useDispatch إمكانية وصول أي مكون من مكونات React إلى الدالة dispatch العائدة لمخزن Redux والمعرّف في الملف "index.js". يسمح هذا الأمر لكل المكونات أن تغير حالة المخزن، فيمكن للمكوِّن الوصول إلى الملاحظات المحفوظة في المخزن باستخدام الخطاف useSelector العائد للمكتبة React-Redux:

import { useSelector, useDispatch } from 'react-redux'
const App = () => {
  // ...
  const notes = useSelector(state => state)  // ...
}

يتلقى الخطاف useSelector دالة كمعامل. فإما أن تبحث هذه الدالة عن البيانات أو انتقائها من مخزن Redux. سنحتاج هنا إلى جميع الملاحظات، لذلك ستعيد دالة الانتقاء حالة المخزن كاملةً:

state => state

وهذا الشكل هو اختصار للشيفرة التالية:

(state) => {
  return state
}

عادةً ما تستخدم دوال الانتقاء للحصول على أجزاء محددة من محتوى المخزن، فيمكننا على سبيل المثال أن نعيد الملاحظات الهامة فقط:

const importantNotes = useSelector(state => state.filter(note => note.important))  

مكوِّنات أكثر

لنفصل شيفرة إنشاء ملاحظة جديدة، ونضعها في مكوّن خاص بها:

import React from 'react'
import { useDispatch } from 'react-redux'import { createNote } from '../reducers/noteReducer'
const NewNote = (props) => {
  const dispatch = useDispatch()
  const addNote = (event) => {
    event.preventDefault()
    const content = event.target.note.value
    event.target.note.value = ''
    dispatch(createNote(content))  
  }
  return (
    <form onSubmit={addNote}>
      <input name="note" />
      <button type="submit">add</button>
    </form>
  )
}

export default NewNote

وعلى خلاف شيفرة React التي تكتب بمعزل عن Redux، نُقل معالج الحدث الذي يغير حالة التطبيق (والذي يقيم الآن في Redux) من المكوّن الجذري إلى المكوّن الابن. فمنطق تغيير الحالة في Redux يبقى مستقلًا تمامًا عن كامل أجزاء تطبيق React.

كما سنفصل أيضًا قائمة الملاحظات وسنعرض كل ملاحظة ضمن مكوّناتها الخاصة (وسيوضع كلاهما في الملف Notes.js):

import React from 'react'
import { useDispatch, useSelector } from 'react-redux'import { toggleImportanceOf } from '../reducers/noteReducer'
const Note = ({ note, handleClick }) => {
  return(
    <li onClick={handleClick}>
      {note.content} 
      <strong> {note.important ? 'important' : ''}</strong>
    </li>
  )
}

const Notes = () => {
const dispatch = useDispatch()  
const notes = useSelector(state => state)
  return(
    <ul>
      {notes.map(note =>
        <Note
          key={note.id}
          note={note}
          handleClick={() => 
            dispatch(toggleImportanceOf(note.id))
          }
        />
      )}
    </ul>
  )
}

export default Notes

ستكون الشيفرة المسؤولة عن تغيير أهمية الملاحظة في المكون الذي يدير قائمة الملاحظات، ولم يبق هناك الكثير من الشيفرة في المكوِّن App:

const App = () => {

  return (
    <div>
      <NewNote />
      <Notes  />
    </div>
  )
}

إن شيفرة المكوّن Note المسؤول عن تصيير ملاحظة واحدة بسيطة جدًا، ولا يمكنها معرفة أن معالج الحدث الذي سيمرر لها كخاصّية سيُوفِد فعلًا. يدعى هذا النوع من المكونات وفق مصطلحات React، بالمكوّن التقديمي presentational.

يمثل المكوّن Notes من ناحية أخرى مكوّن احتواء container. فهو يحتوي شيئًا من منطق التطبيق. إذ يُعرِّف ما سيفعله معالج حدث المكوِّن Note ويضبط تهيئة المكوّنات التقديمية وهي في حالتنا المكوّن Note.

سنعود إلى مفهومي مكون الاحتواء والمكون التقديمي لاحقًا في هذا القسم.

يمكنك الحصول على شيفرة تطبيق Redux في الفرع part6-1 ضمن المستودع الخاص بالتطبيق على Github.

التمارين 6.3 - 6.8

لننشئ نسخة جديدة من تطبيق التصويت على الطرائف الذي كتبناه في القسم 1. يمكنك الحصول على المشروع من المستودع التالي https://github.com/fullstack-hy2020/redux-anecdotes كأساس لتطبيقك.

إن نسخت المشروع إلى مستودع git موجود مسبقًا أزل تهيئة git لنسختك كالتالي:

cd redux-anecdotes  // الذهاب إلى مستودع نسختك من التطبيق
rm -rf .git

يمكن أن تشغل التطبيق بالطريق الاعتيادية، لكن عليك أولًا تثبيت الاعتماديات اللازمة:

npm install
npm start

بعد إكمال هذه التمارين سيكون لتطبيقك شكلًا مشابهًا للتالي:

ancedotes_redux_05.png

6.3 طرائف: الخطوة 1

أضف وظيفة التصويت على الطرائف. يجب عليك تخزين نتيجة التصويت في مخزن Redux.

6.4 طرائف: الخطوة 2

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

6.5 طرائف: الخطوة 3 *

تأكد أنّ الطرائف قد رتبت حسب عدد الأصوات.

6.6 طرائف: الخطوة 4

إن لم تكن قد فعلت ذلك، إفصل شيفرة إنشاء كائنات الأفعال وضعها في دالة توليد أفعال، ثم ضعهم في الملف src/reducers/anecdoteReducer.js. يمكنك اتباع الطريقة التي استخدمناها في هذا الفصل، بعد أن تعرفنا على مولدات الأفعال.

6.7 طرائف: الخطوة 7

أنشئ مكوِّنًا جديدًا وسمّه AnecdoteForm، ثم ضع شيفرة إنشاء طرفة جديدة ضمنه.

6.8 طرائف: الخطوة 8

أنشئ مكوّنًا جديدًا وسمّه AnecdoteList، ثم انقل شيفرة تصيير قائمة الطرائف إليه.

سيبدو محتوى المكوّن App الآن مشابهًا للتالي:

import React from 'react'
import AnecdoteForm from './components/AnecdoteForm'
import AnecdoteList from './components/AnecdoteList'

const App = () => {
  return (
    <div>
      <h2>Anecdotes</h2>
      <AnecdoteForm />
      <AnecdoteList  />
    </div>
  )
}

export default App

ترجمة -وبتصرف- للفصل Flux-architecture and Redux من سلسلة 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.


×
×
  • أضف...