full_stack_101 تغيير البيانات على الخادم في تطبيقات React


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

من الطبيعي عندما ننشئ ملاحظات جديدة في تطبيقنا، أن نحفظ هذه الملاحظات على الخادم. تَعتبِر حزمة خادم JSON نفسها على أنها واجهة REST أو RESTful كما ورد في توثيقها:

اقتباس

احصل على واجهة تطبيقات وهمية متوافقة مع REST وبدون كتابة أية شيفرات خلال 30 ثانية.

لا يتطابق توصيف خادم JSON تمامًا مع تعريف واجهة التطبيقات REST، كما هي حال معظم الواجهات التي تدّعي بأنها متوافقة تمامًا مع REST. سنطلع أكثر على REST في القسم التالي من المنهاج، لكن من المهم أن نتعلم في هذه المرحلة بعض المعايير التوافقية المستخدمة من قبل خادم JSON والواجهة REST بشكل عام. وسنلقي نظرة خاصة على الاستخدام التوافقي للمسارات (Routes) وهي الروابط وطلبات HTTP وفق مفاهيم REST.

واجهة التطبيقات REST

نشير إلى الكائنات التي تمثل بيانات منفردة -كالملاحظات في تطبيقنا- باسم الموارد (resources) وفقًا لمصطلحات REST. ولكل مورد مكانه المحدد والوحيد الذي يرتبط به (URL الخاص به). وبناء على معيار عام يعتمده خادم JSON، سنتمكن من الحصول على ملاحظة واحدة من الملاحظات من المورد الموجود في الموقع notes/3. حيث يمثل الرقم 3 المعرِّف الفريد للمورد. ومن ناحية أخرى سيمثل موقع المورد notes تجمّع موارد (resource collection) يضم كل الملاحظات.

تُحضَر الموارد من الخادم عن طريق الطلبات HTTP-GET. فالطلب HTTP-GET المرسل إلى الموقع notes/3 سيعيد الملاحظة التي معرِّفها 3. بينما لو أُرسل نفس الطلب إلى الموقع notes سيعيد لائحة بالملاحظات الموجودة.

يُنشأ مورد جديد لتخزين ملاحظة بإرسال طلب HTTP-POST إلى الموقع notes، وفقًا لمعيار REST الذي يتوافق معه خادم JSON. ترسل البيانات إلى المورد الجديد "الملاحظة الجديدة" ضمن جسم الطلب. يفرض خادم JSON إرسال جميع البيانات بتنسيق JSON. ويقضي ذلك عمليًا أن تكون البيانات على شكل نص منسق بشكل صحيح وأن يضم الطلب ترويسة "نوع المحتوى" بداخلها القيمة application/json.

إرسال البيانات إلى الخادم

سنعدّل معالج الحدث المسؤول عن إنشاء ملاحظة جديدة كالتالي:

addNote = event => {
  event.preventDefault()
  const noteObject = {
    content: newNote,
    date: new Date(),
    important: Math.random() < 0.5,
  }

  axios    
    .post('http://localhost:3001/notes', noteObject)
      .then(response => {
      console.log(response)
  })}

لقد أنشأنا كائنًا جديدًا يحتوي بيانات الملاحظة لكننا أغفلنا الخاصية id، فمن الأفضل أن يولد الخادم قيم المعرّفات الفريدة للموارد id. يُرسل الكائن بعد ذلك إلى الخادم باستخدام التابع post من المكتبة axios. ثم يطبع معالج الحدث المعرّف مسبقًا استجابة الخادم على الطرفية. ستعرض الطرفية البيانات التالية عند إنشاء ملاحظة جديدة:

data_from_new_note_001.png

يخزَّن مورد الملاحظة الجديدة في الخاصية data لكائن الاستجابة الذي يعيده الخادم. من المفيد أحيانًا تحرّي طلبات HTTP في النافذة Network من طرفية تطوير المتصفح الخاص بك والتي استخدمناها كثيرًا في مقال أساسيات بناء تطبيقات الويب.

using_console_network_002.png

باستخدام المفتش inspector سنتمكن من تحري الترويسات التي ترسل عبر طلبات post فيما لو كانت هي تمامًا ما نريده، أو أنّ القيم التي تحملها صحيحة. تسنِد axios تلقائيًا القيمة "application/json" إلى ترويسة "نوع-المحتوى"، طالما أن البيانات المرسلة عبر الطلب post هي كائن JavaScript.

لم تصيّر الملاحظة الجديدة على الشاشة بعد، وذلك لأننا لم نحدث حالة المكوِّنApp بعد إنشائها، لذا سنصلح الأمر:

addNote = event => {
  event.preventDefault()
  const noteObject = {
    content: newNote,
    date: new Date(),
    important: Math.random() > 0.5,
  }

  axios
    .post('http://localhost:3001/notes', noteObject)
    .then(response => {
      setNotes(notes.concat(response.data))
      setNewNote('')
  })
}

تضاف الملاحظة الجديدة القادمة من الخادم إلى قائمة الملاحظات في تطبيقنا باستخدام الدالة setNotes، وبعدها تصفّر الشيفرة نموذج إنشاء الملاحظات. وتذكر أحد التفاصيل المهمة عند استخدام التابع concat، بأنه لا يغيّر المصفوفة الأصلية وبالتالي لا يغيّر حالة المكوِّن بل ينشئ نسخة عن القائمة الأصلية. سيبدأ تأثير الخادم على سلوك تطبيقنا لحظة إعادته البيانات. وسنكون مباشرة أمام تحديات جديدة منها على سبيل المثال عدم التزامن في الاتصال. لهذا ستظهر الحاجة إلى استراتيجيات جديدة للتنقيح بالإضافة إلى الطباعة على الطرفية، وغيرها من الطرق التنقيح التي تزداد أهميتها مع الوقت. إضافة إلى ذلك لابد من فهمٍ كافٍ لمبادئ بيئة تشغيل JavaScript ومكوِّنات React. فلن يكفي التخمين فقط في حل المشاكل. ومن المفيد أغلب الأحيان التحقق من حالة الخادم من خلال المتصفح:

server_inspect_003..png

حيث يمكننا التحقق من أنّ جميع البيانات التي أرسلناها قد استقبلها الخادم.

سنتعلم في القسم التالي من المنهاج كيف نتعامل بمنطقنا الخاص مع الواجهة الخلفية، وسنلقي نظرة أقرب على أدوات مهمة مثل postman التي ستساعد في تنقيح تطبيقات الواجهة الخلفية. يكفينا حاليًا تحرّي حالة الخادم من خلال المتصفح.

اقتباس

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

يمكن الحصول على شيفرة التطبيق بوضعه الحالي في الفرع part2-5 على github.

تغيير مؤشر أهمية الملاحظة

لنضف زرًا إلى كل ملاحظة بحيث نتمكن من تغيير حالتها بشكل مستمر، ستتغير الشيفرة في المكوِّن Note كما يلي:

const Note = ({ note, toggleImportance }) => {
  const label = note.important
    ? 'make not important' : 'make important'

  return (
    <li>
      {note.content} 
      <button onClick={toggleImportance}>{label}</button>
    </li>
  )
}

لقد أضفنا عنصر button إلى المكوِّن، وعيّنا الدالة toggleImportance كمعالج لحدث النقر على الزر، ومررناه إلى المكوِّن من خلال الخصائص. يعرّف المكوِّن App نسخة ابتدائية من معالج الحدث toggleImportanceOf، ثم يمرره إلى كل مكوِّن Note:

const App = () => {
  const [notes, setNotes] = useState([]) 
  const [newNote, setNewNote] = useState('')
  const [showAll, setShowAll] = useState(true)

  // ...

  const toggleImportanceOf = (id) => {
    console.log('importance of ' + id + ' needs to be toggled')
  }
  // ...

  return (
    <div>
      <h1>Notes</h1>
      <div>
        <button onClick={() => setShowAll(!showAll)}>
          show {showAll ? 'important' : 'all' }
        </button>
      </div>      
      <ul>
        {notesToShow.map((note, i) => 
          <Note
            key={i}
            note={note} 
            toggleImportance={() => toggleImportanceOf(note.id)}          />
        )}
      </ul>
      // ...
    </div>
  )
}

لاحظ في الشيفرة السابقة كيف ستحصل كل ملاحظة على معالج حدث فريد خاص بها، ذلك أن قيمة id لكل ملاحظة هي قيمة فريدة. فلو افترضنا أن قيمة id لملاحظة هي 3، ستكون دالة معالج الحدث الخاصة بها والتي تعيدها الدالة (toggleImportance(note.id من الشكل:

() => { console.log('importance of 3 needs to be toggled') }

وتذكر هنا أننا استخدمنا أسلوبًا مشابهًا للغة Java في طباعة النص على الطرفية، وذلك بجمع السلاسل النصية:

console.log('importance of ' + id + ' needs to be toggled')

كما يمكنك استخدام القالب النصي الذي أضيف مع ES6، لكتابة سلسلة نصية مماثلة وبطريقة أجمل:

console.log(`importance of ${id} needs to be toggled`)

إذا وضعنا عبارة JavaScript بين قوسين معقوصين وقبلها رمز الدولار($) ستُنفَّذ هذه العبارة ضمن السلسلة النصية وتُطبَع قيمتها. وانتبه جيدًا إلى علامات التنصيص المستخدمة في القالب النصي (```

نستطيع التعديل على الملاحظة المخزنة على خادم JSON بطريقتين مختلفتين، وذلك بإرسال طلبات HTTP إلى الموقع الفريد للملاحظة. إذ بالإمكان أن نستبدل الملاحظة بالكامل من خلال الطلب HTTP-PUT، أو أن نغير بعض حقولها من خلال الطلب HTTP-PATCH.

سيبدو الشكل النهائي لدالة معالج الحدث كالتالي:

const toggleImportanceOf = id => {
  const url = `http://localhost:3001/notes/${id}`
  const note = notes.find(n => n.id === id)
  const changedNote = { ...note, important: !note.important }

  axios.put(url, changedNote).then(response => {
    setNotes(notes.map(note => note.id !== id ? note : response.data))
  })
}

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

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

تنشئ التعليمة {note...} كائنًا جديدًا يحمل نسخًا عن جميع خصائص (حقول) الكائن note. وعند وضع الخصائص ضمن قوسين معقوصين بعد نشر الكائن على الشكل التالي {note, important: true …}، ستأخذ الخاصة important للكائن الجديد القيمة true. لكن انتبه إلى مثالنا السابق بأننا أخذنا القيمة المعاكسة للخاصة important. هناك نقاط أخرى لابد من الإشارة إليها، فلماذا مثلًا أنشأنا نسخة عن الملاحظة التي نريد تعديلها إن كان بالإمكان تنفيذ ذلك كما يلي:

const note = notes.find(n => n.id === id)
note.important = !note.important

axios.put(url, note).then(response => {
  // ...

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

بالعودة إلى تفاصيل دالة معالج حدث تغيير أهمية الملاحظة، ستُرسل أخيرًا الملاحظة الجديدة إلى الخادم على شكل طلب HTTP-PUT، وتُستبدل الملاحظة القديمة. تضبط دالة الاستدعاء (إعادة النداء) حالة الملاحظات في المكوِّن من جديد على قيم المصفوفة التي تضم جميع الملاحظات ماعدا تلك التي تم استبدالها بالنسخة الجديدة التي يعيدها الخادم:

axios.put(url, changedNote).then(response => {
  setNotes(notes.map(note => note.id !== id ? note : response.data))
})

وينجز ذلك باستخدام التابع map:

notes.map(note => note.id !== id ? note : response.data)

يعيد التابع map مصفوفة جديدة عن طريق ربط كل عنصر من المصفوفة القديمة بعنصر من المصفوفة الجديدة. لكن إنشاء المصفوفة الجديدة في مثالنا كان شرطيًا، فإن تحقق الشرط التالي note.id !==id ننقل العنصر من المصفوفة القديمة إلى الجديدة، وإن لم يتحقق ستحل محلها الملاحظة الجديدة التي يعيدها الخادم. قد تبدو حيلة map هذه غريبةً قليلًا، لكنها تستحق التوقف عندها وفهمها لأننا سنستخدمها مرات عدة لاحقًا.

نقل تعليمات الاتصال مع الواجهة الخلفية إلى وحدة منفصلة

لقد أصبح المكوِّنApp مليئًا بالشيفرات بعد إضافة الاتصال مع الواجهة الخلفية. وانسجامًا مع مبدأ المسؤولية الفردية، وجدنا من الحكمة أن ننقل الشيفرات المتعلقة بالاتصال إلى وحدة منفصلة خاصة بها. لننشئ مجلدًا باسم services ضمن المجلد src وننشئ ضمنه الملف notes.js:

import axios from 'axios'
const baseUrl = 'http://localhost:3001/notes'

const getAll = () => {
  return axios.get(baseUrl)
}

const create = newObject => {
  return axios.post(baseUrl, newObject)
}

const update = (id, newObject) => {
  return axios.put(`${baseUrl}/${id}`, newObject)
}

export default { 
  getAll: getAll, 
  create: create, 
  update: update 
}

تعيد الوحدة كائنًا من ثلاث دوال getAll وcreate و update، بالإضافة إلى خصائصه التي تتعامل مع الملاحظات. تعيد تلك الدوال وبشكل مباشر الوعود الناتجة عن تنفيذ توابع المكتبة axios والمتعلقة بالاتصال مع الخادم. يدرج المكوِّن App الوحدة الجديدة باستخدام التعليمة import:

import noteService from './services/notes'
const App = () => {

يمكن استخدام الدوال مباشرة بإسنادها للمتغير noteService عند إدراج الوحدة كما يلي:

const App = () => {
  // ...

  useEffect(() => {
    noteService
        .getAll()
        .then(response => {
        setNotes(response.data)
    })
  }, [])

  const toggleImportanceOf = id => {
    const note = notes.find(n => n.id === id)
    const changedNote = { ...note, important: !note.important }

    noteService
        .update(id, changedNote)
        .then(response => {
        setNotes(notes.map(note => note.id !== id ? note : response.data))
      })
  }

  const addNote = (event) => {
    event.preventDefault()
    const noteObject = {
      content: newNote,
      date: new Date().toISOString(),
      important: Math.random() > 0.5
    }

    noteService
        .create(noteObject)
        .then(response => {
        setNotes(notes.concat(response.data))
        setNewNote('')
    })
  }

  // ...
}

export default App

سننقل التطبيق خطوة إضافية إلى الأمام بحيث يتلقى المكوِّن App كائنًا يحتوي على الاستجابة الكاملة للخادم على طلب HTTP في حال استخدم المكوِّن الدوال السابقة:

noteService
  .getAll()
  .then(response => {
    setNotes(response.data)
  })

يستخدم المكوِّن App الخاصة response.data فقط من كائن الاستجابة. وبالتالي سيكون استخدام الوحدة مريحًا أكثر لو أمكننا أن نحصل على بيانات الاستجابة فقط بدلًا من الاستجابة كاملةً. ستبدو الشيفرة التي تنفذ ذلك على النحو:

noteService
  .getAll()
  .then(initialNotes => {
    setNotes(initialNotes)
  })

يمكننا إنجاز ذلك بتغيير الشيفرة في الوحدة على النحو التالي:

import axios from 'axios'
const baseUrl = 'http://localhost:3001/notes'

const getAll = () => {
  const request = axios.get(baseUrl)
  return request.then(response => response.data)
}

const create = newObject => {
  const request = axios.post(baseUrl, newObject)
  return request.then(response => response.data)
}

const update = (id, newObject) => {
  const request = axios.put(`${baseUrl}/${id}`, newObject)
  return request.then(response => response.data)
}

export default { 
  getAll: getAll, 
  create: create, 
  update: update 
}

وهكذا فإن دوال وحدة الاتصال لن تعيد وعود توابع axios مباشرة، بل تسند الوعد إلى المتغيّر request ثم تستدعي التابع then الموافق:

const getAll = () => {
  const request = axios.get(baseUrl)
  return request.then(response => response.data)
}

بالنسبة للسطر الأخير من الشيفرة السابقة فهو شكل أكثر اختصارًا للشيفرة التالية:

const getAll = () => {
  const request = axios.get(baseUrl)
  return request.then(response => {    
    return response.data
  })}

تعيد الدالة المعدّلة getAll وعدًا، وكذلك سيفعل التابع then الذي ينتج عن وعد ويعيد وعدًا. فبعد أن نعرّف معامل التابع then ليعيد مباشرة بيانات الاستجابة response.data نكون قد حصلنا على غايتنا من استخدام الدالة getAll. إن نجح طلب HTTP سيعيد الوعد البيانات التي يرسلها الخادم ضمن استجابته للطلب.

سنحدّث الآن شيفرة المكوِّن App ليعمل مع التغييرات التي أجريناها على وحدة الاتصال. سنبدأ بإصلاح دوال الاستدعاء التي تُمرّر كمعاملات إلى توابع الكائن noteService حتى تتمكن من إعادة بيانات الاستجابة من الخادم مباشرة:

const App = () => {
  // ...

  useEffect(() => {
    noteService
      .getAll()
        .then(initialNotes => {
        setNotes(initialNotes)
      })
  }, [])

  const toggleImportanceOf = id => {
    const note = notes.find(n => n.id === id)
    const changedNote = { ...note, important: !note.important }

    noteService
      .update(id, changedNote)
        .then(returnedNote => {
        setNotes(notes.map(note => note.id !== id ? note : returnedNote))
      })
  }

  const addNote = (event) => {
    event.preventDefault()
    const noteObject = {
      content: newNote,
      date: new Date().toISOString(),
      important: Math.random() > 0.5
    }

    noteService
      .create(noteObject)
        .then(returnedNote => {
        setNotes(notes.concat(returnedNote))
        setNewNote('')
      })
  }

  // ...
}

تبدو الأمور معقدة، وقد تزداد تعقيدًا عندما نحاول أن نتعمق في شرحها، لذلك حاول أن تطلع على هذا الموضوع أكثر. وبالطبع شرحت أكاديمية حسوب شرحًا وافيًا هذا الموقع في مقال، سلسلة الوعود في جافاسكربت وستجد شرحًا وافيًا ومطولًا عن الموضوع في المراجع الأجنبية مثلًا في كتاب "Async performance" وهو أحد كتب سلسلة You do not know JS.

إنّ فكرة الوعود هي فكرة مركزية في تطوير تطبيقات JavaScript الحديثة، لذلك ننصحك بشدة أن تقضي وقتًا كافيًا في فهمها.

كتابة شيفرة أوضح عند تعريف الكائنات المجرّدة

تُصَدِّر الوحدة التي أنشأناها سابقًا والتي تعرّف خدمات تتعلق بالتعامل مع الملاحظات، كائنًا يمتلك الخصائص getAll وcreate وupdate والتي تسند إلى دوال مخصصة للتعامل مع الملاحظات. يبدو تعريف هذه الوحدة بالشكل التالي:

import axios from 'axios'
const baseUrl = 'http://localhost:3001/notes'

const getAll = () => {
  const request = axios.get(baseUrl)
  return request.then(response => response.data)
}

const create = newObject => {
  const request = axios.post(baseUrl, newObject)
  return request.then(response => response.data)
}

const update = (id, newObject) => {
  const request = axios.put(`${baseUrl}/${id}`, newObject)
  return request.then(response => response.data)
}

export default { 
  getAll: getAll, 
  create: create, 
  update: update 
}

تصدَّر الوحدة الكائن التالي:

{ 
  getAll: getAll, 
  create: create, 
  update: update 
}

تدعى العبارات التي تقع على يسار العمود في الشيفرة السابقة، مفاتيح الكائن. بينما تدعى العبارات التي تقع على اليمين بمتغيّرات الكائن والتي تُعرّف داخل الوحدة. وطالما أن المفاتيح والمتغيرات تحمل نفس التسمية، يمكننا صياغة الكائن بشكل أكثر إختصارًا:

{ 
  getAll, 
  create, 
  update 
}

وكنتيجة لذلك سيصبح تعريف الوحدة أبسط:

import axios from 'axios'
const baseUrl = 'http://localhost:3001/notes'

const getAll = () => {
  const request = axios.get(baseUrl)
  return request.then(response => response.data)
}

const create = newObject => {
  const request = axios.post(baseUrl, newObject)
  return request.then(response => response.data)
}

const update = (id, newObject) => {
  const request = axios.put(`${baseUrl}/${id}`, newObject)
  return request.then(response => response.data)
}

export default { getAll, create, update }

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

const name = 'Leevi'
const age = 0

لقد توجب علينا في النسخ القديمة من JavaScript أن نعرف الكائن على النحو التالي:

const person = {
  name: name,
  age: age
}

وطالما أن الخصائص والمتغيرات في الكائن لها نفس التسمية، سيكون كافيًا كتابة التعريف باستخدام ES6 كالتالي:

const person = { name, age }

ستكون النتيجة نفسها في الطريقتين، فكلاهما يعرفان كائنًا له خاصية تدعى name تحمل القيمة "Leevi" وخاصية تدعى "age" وتحمل القيمة 0.

الوعود والأخطاء

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

const getAll = () => {
  const request = axios.get(baseUrl)
  const nonExisting = {
    id: 10000,
    content: 'This note is not saved to server',
    date: '2019-05-30T17:30:31.098Z',
    important: true,
  }
  return request.then(response => response.data.concat(nonExisting))
}

عندما نحاول تغيير أهمية تلك الملاحظة، ستظهر لنا رسالة خطأ على شاشة الطرفية مفادها أن الخادم قد استجاب لطلب HTTP برمز الحالة 404 (غير موجود).

console_not_found_004.png

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

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

axios
  .get('http://example.com/probably_will_fail')
  .then(response => {
    console.log('success!')
  })
  .catch(error => {
    console.log('fail')
  })

عندما يخفق الطلب، يستدعى معالج الحدث المعرف ضمن التابع catch الذي يوضع عادة في آخر سلسلة التوابع التي تتعامل مع الوعد. فعندما يرسل التطبيق طلب HTTP، سينشأ في الواقع ما يسمى سلسلة الوعد كما في الشيفرة التالية:

axios
  .put(`${baseUrl}/${id}`, newObject)
  .then(response => response.data)
  .then(changedNote => {
    // ...
  })

وهكذا يُستدعى التابع catch حالما يرمي أي تابع في سلسلة الوعد خطأ ويرفض الوعد:

axios
  .put(`${baseUrl}/${id}`, newObject)
  .then(response => response.data)
  .then(changedNote => {
    // ...
  })
  .catch(error => {
    console.log('fail')
  })

لنستفد من الميزة السابقة ولنعرّّف معالجًا للخطأ في المكوِّن App:

const toggleImportanceOf = id => {
  const note = notes.find(n => n.id === id)
  const changedNote = { ...note, important: !note.important }

  noteService
    .update(id, changedNote).then(returnedNote => {
      setNotes(notes.map(note => note.id !== id ? note : returnedNote))
    })
    .catch(error => {
      alert(
          `the note '${note.content}' was already deleted from server`
      )
      setNotes(notes.filter(n => n.id !== id))
  })}

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

notes.filter(n => n.id !== id)

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

يمكن الحصول على شيفرة التطبيق بوضعه الحالي في الفرع part2-6 على github.

التمارين 2.15-2.18

2.15 دليل الهاتف: الخطوة 7

لنعد إلى تطبيق دليل الهاتف الذي بدأناه سابقًا. احفظ رقم الهاتف الذي ندخله في الخادم.

2.16 دليل الهاتف: الخطوة 8

انقل الشيفرات التي تؤمن الاتصال مع الواجهة الخلفية إلى وحدة خاصة بها، بالاستفادة من المثال الذي عرضناه سابقًا في هذا الجزء.

2.17 دليل الهاتف: الخطوة 9

امنح المستخدم إمكانية حذف المدخلات إلى دليل الهاتف. يمكن أن تنفذ ذلك باستخدام زر خاص يظهر بجوار كل اسم في الدليل. يمكنك الطلب من المستخدم تأكيد عملية الحذف باستخدام التابع window.confirm:

phonebook_step9_005.png

يمكن حذف المورد المتعلق بشخص من الخادم باستخدام طلب HTTP-DELETE إلى موقع المورد. فلو أردت حذف الشخص الذي قيمة معرّفه id هي 2، نفذ ذلك باستخدام الطلب السابق إلى الموقع localhost:3001/persons/2. وتذكر أنه لا تُرسل أية بيانات مع هذا الطلب.

يمكنك استخدام الطلب HTTP-DELETE مع توابع المكتبة axios بنفس الطريقة التي استخدمناها فيها.

انتبه لا يمكن اختيار "delete" كاسم لمتغيّر، لأنها كلمة محجوزة في JavaScript. فالشيفرة التالية مثلًا غير صحيحة:

// استخدم اسما آخر للمتغير
const delete = (id) => {
  // ...
}

2.18 دليل الهاتف: الخطوة 10 *

أضف ميزة إلى تطبيقك تستبدل فيها الرقم القديم بالرقم الجديد إذا أضاف المستخدم رقمًا إلى شخص موجود مسبقًا. يفضل استخدام طلب HTTP-PUT لتنفيذ هذا الأمر. ليطلب تطبيقك من المستخدم أن يؤكد العملية إذا كانت معلومات الشخص موجودة مسبقًا.

phonebook_step10_006.png

ترجمة -وبتصرف- للفصل Altering Data in Server من سلسلة Deep Dive Into Modern Web Development





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


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



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن