تعديل البيانات على الخادم
عند إنشاء الملاحظات في تطبيقنا، سنرغب بطبيعة الحال في تخزينها في خادم خلفي ما. تدّعي حزمة json-server في توثيقها أنها ما يُسمى واجهة REST أو RESTful API:
احصل على واجهة REST وهمية كاملة دون كتابة أي شيفرة في أقل من 30 ثانية (بجد)
لا يطابق json-server تماماً الوصف الوارد في التعريف الأكاديمي لواجهة REST API، لكن هذا ينطبق أيضاً على معظم الواجهات الأخرى التي تدّعي أنها RESTful.
سنلقي نظرة أقرب على REST في الجزء التالي من الدورة. لكن من المهم أن نتعرّف في هذه المرحلة على بعض الأعراف التي يستخدمها json-server وواجهات REST API عموماً. وسننظر تحديداً في الاستخدام التقليدي لـالمسارات، أي عناوين URL وأنواع طلبات HTTP، في REST.
REST
في مصطلحات REST، نشير إلى كائنات البيانات الفردية، مثل الملاحظات في تطبيقنا، باسم الموارد. ولكل مورد عنوان فريد مرتبط به — عنوان URL الخاص به. ووفقاً لعُرف عام يستخدمه json-server، يمكننا تحديد موقع ملاحظة فردية عند عنوان المورد notes/3، حيث 3 هو معرّف المورد. أما عنوان notes فيشير إلى مجموعة موارد تحتوي على جميع الملاحظات.
تُجلب الموارد من الخادم عبر طلبات HTTP GET. فمثلاً، طلب HTTP GET إلى عنوان notes/3 سيعيد الملاحظة ذات المعرّف رقم 3. وطلب HTTP GET إلى عنوان notes سيعيد قائمة بجميع الملاحظات.
ويتم إنشاء مورد جديد لتخزين ملاحظة عبر إرسال طلب HTTP POST إلى عنوان notes وفقاً لعُرف REST الذي يلتزم به json-server. وتُرسَل بيانات مورد الملاحظة الجديد في جسم الطلب.
يتطلب json-server إرسال جميع البيانات بصيغة JSON. ويعني هذا عملياً أن تكون البيانات نصاً منسّقاً بشكل صحيح، وأن يحتوي الطلب على ترويسة الطلب Content-Type بالقيمة application/json.
إرسال البيانات إلى الخادم
لنُجرِ التغييرات التالية على معالج الحدث المسؤول عن إنشاء ملاحظة جديدة:
const addNote = event => {
event.preventDefault()
const noteObject = {
content: newNote,
important: Math.random() < 0.5,
}
// highlight-start
axios
.post('http://localhost:3001/notes', noteObject)
.then(response => {
console.log(response)
})
// highlight-end
}
ننشئ كائناً جديداً للملاحظة لكننا نحذف خاصية id لأن من الأفضل أن نترك للخادم توليد المعرّفات لمواردنا.
يُرسَل الكائن إلى الخادم باستخدام طريقة post في axios. ويسجّل معالج الحدث المُسجَّل الاستجابةَ العائدة من الخادم في وحدة التحكم.
عندما نحاول إنشاء ملاحظة جديدة، يظهر الناتج التالي في وحدة التحكم:

يُخزَّن مورد الملاحظة المنشأ حديثاً في قيمة خاصية data في كائن response.
غالباً ما يكون من المفيد فحص طلبات HTTP في تبويب Network ضمن أدوات مطوّري Chrome، وقد استُخدم هذا بكثافة في بداية الجزء 0.
يمكننا استخدام أداة الفحص للتأكد من أن الترويسات المرسَلة في طلب POST هي ما توقعناه:

ولأن البيانات التي أرسلناها في طلب POST كانت كائن JavaScript، عرف axios تلقائياً أنه يجب ضبط القيمة المناسبة application/json لترويسة Content-Type.
ويمكن استخدام تبويب payload للتحقق من بيانات الطلب:

كما أن تبويب response مفيد أيضاً، فهو يُظهر البيانات التي استجاب بها الخادم:

لم تُعرَض الملاحظة الجديدة على الشاشة بعد. والسبب أننا لم نحدّث حالة مكوّن App عند إنشائها. لنصلح هذا:
const addNote = event => {
event.preventDefault()
const noteObject = {
content: newNote,
important: Math.random() > 0.5,
}
axios
.post('http://localhost:3001/notes', noteObject)
.then(response => {
// highlight-start
setNotes(notes.concat(response.data))
setNewNote('')
// highlight-end
})
}
تُضاف الملاحظة الجديدة التي أعادها الخادم الخلفي إلى قائمة الملاحظات في حالة تطبيقنا بالطريقة المعتادة باستخدام الدالة setNotes ثم إعادة ضبط نموذج إنشاء الملاحظة. ومن التفاصيل المهمة التي ينبغي تذكّرها أن طريقة concat لا تغيّر الحالة الأصلية للمكوّن، بل تنشئ نسخة جديدة من القائمة.
بمجرد أن تبدأ البيانات التي يعيدها الخادم بالتأثير على سلوك تطبيقات الويب لدينا، نواجه فوراً مجموعة جديدة كاملة من التحديات الناشئة عن أمور مثل لاتزامن التواصل (asynchronicity). ويستلزم ذلك استراتيجيات جديدة لتصحيح الأخطاء، كما يصبح التسجيل في وحدة التحكم ووسائل تصحيح الأخطاء الأخرى أكثر أهمية. ويجب أيضاً أن نطوّر فهماً كافياً لمبادئ كل من بيئة تشغيل JavaScript ومكوّنات React. فلن يكفي التخمين.
من المفيد فحص حالة الخادم الخلفي، مثلاً عبر المتصفح:

وهذا يجعل من الممكن التحقق من أن جميع البيانات التي قصدنا إرسالها قد استقبلها الخادم فعلاً.
في الجزء التالي من الدورة، سنتعلم تنفيذ منطقنا الخاص في الواجهة الخلفية. وسنلقي حينها نظرة أقرب على أدوات مثل Postman التي تساعدنا في تصحيح أخطاء تطبيقات الخادم. غير أن فحص حالة json-server عبر المتصفح كافٍ لاحتياجاتنا الحالية.
يمكن العثور على شيفرة الحالة الحالية لتطبيقنا في الفرع 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>
)
}
نضيف زراً إلى المكوّن ونسند إليه معالج الحدث وهو الدالة toggleImportance الممرَّرة في props الخاصة بالمكوّن.
يعرّف مكوّن App نسخة أولية من دالة معالج الحدث toggleImportanceOf ويمرّرها إلى كل مكوّن Note:
const App = () => {
const [notes, setNotes] = useState([])
const [newNote, setNewNote] = useState('')
const [showAll, setShowAll] = useState(true)
// ...
// highlight-start
const toggleImportanceOf = (id) => {
console.log('importance of ' + id + ' needs to be toggled')
}
// highlight-end
// ...
return (
<div>
<h1>Notes</h1>
<div>
<button onClick={() => setShowAll(!showAll)}>
show {showAll ? 'important' : 'all' }
</button>
</div>
<ul>
{notesToShow.map(note =>
<Note
key={note.id}
note={note}
toggleImportance={() => toggleImportanceOf(note.id)}
/>
)}
</ul>
// ...
</div>
)
}
لاحظ كيف تتلقى كل ملاحظة دالة معالج حدث فريدة خاصة بها بما أن id كل ملاحظة فريد.
فمثلاً، إذا كانت note.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، مثل قيمة متغير. لاحظ أننا نستخدم علامات الاقتباس الخلفية (backticks) في سلاسل القوالب بدلاً من علامات الاقتباس المستخدمة في نصوص JavaScript العادية.
يمكن تعديل الملاحظات الفردية المخزّنة في خادم json-server بطريقتين مختلفتين عبر إرسال طلبات HTTP إلى عنوان URL الفريد للملاحظة. فيمكننا إما استبدال الملاحظة بأكملها بطلب 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 ? response.data : note))
})
}
تحتوي كل سطر تقريباً من الشيفرة في جسم الدالة على تفاصيل مهمة. يعرّف السطر الأول عنوان URL الفريد لكل مورد ملاحظة بناءً على معرّفه.
تُستخدم طريقة find في المصفوفة للعثور على الملاحظة التي نريد تعديلها، ثم نسندها إلى المتغير note.
بعد ذلك، ننشئ كائناً جديداً نسخة طبق الأصل من الملاحظة القديمة، باستثناء خاصية important التي تُقلب قيمتها (من true إلى false أو من false إلى true).
قد تبدو الشيفرة الخاصة بإنشاء الكائن الجديد التي تستخدم صياغة نشر الكائن غريبة بعض الشيء في البداية:
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.
يجدر بالذكر أيضاً أن الكائن الجديد changedNote هو مجرد ما يُسمى نسخة سطحية، أي أن قيم الكائن الجديد هي نفس قيم الكائن القديم. ولو كانت قيم الكائن القديم كائنات بحد ذاتها، لكانت القيم المنسوخة في الكائن الجديد تشير إلى الكائنات نفسها التي كانت في الكائن القديم.
ثم تُرسَل الملاحظة الجديدة بطلب PUT إلى الواجهة الخلفية حيث ستحل محل الكائن القديم.
تضبط دالة الاستدعاء الراجعة حالة notes للمكوّن إلى مصفوفة جديدة تحتوي على جميع العناصر من مصفوفة notes السابقة، باستثناء الملاحظة القديمة التي تُستبدل بالنسخة المحدَّثة منها التي يعيدها الخادم:
axios.put(url, changedNote).then(response => {
setNotes(notes.map(note => note.id === id ? response.data : note))
})
ويتحقق هذا بطريقة map:
notes.map(note => note.id === id ? response.data : note)
تنشئ طريقة map مصفوفة جديدة بتحويل كل عنصر من المصفوفة القديمة إلى عنصر في المصفوفة الجديدة. وفي مثالنا، تُنشأ المصفوفة الجديدة شرطياً بحيث إذا كانت note.id === id صحيحة، يُضاف كائن الملاحظة الذي أعاده الخادم إلى المصفوفة. وإذا كان الشرط خاطئاً، فحينها ننسخ العنصر ببساطة من المصفوفة القديمة إلى المصفوفة الجديدة بدلاً من ذلك.
قد تبدو حيلة map هذه غريبة بعض الشيء في البداية، لكن يجدر قضاء بعض الوقت في استيعابها. وسنستخدم هذه الطريقة مرات عديدة طوال الدورة.
فصل التواصل مع الواجهة الخلفية في وحدة منفصلة
أصبح مكوّن App متضخماً بعض الشيء بعد إضافة الشيفرة الخاصة بالتواصل مع الخادم الخلفي. ووفقاً لروح مبدأ المسؤولية الواحدة، نرى من الحكمة فصل هذا التواصل في وحدة خاصة به.
لننشئ مجلداً src/services ونضف فيه ملفاً باسم 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) كخصائص تتعامل مع الملاحظات. وتعيد الدوال مباشرةً الـpromises التي تعيدها طرق axios.
يستخدم مكوّن App العبارة import للوصول إلى الوحدة:
import noteService from './services/notes'
const App = () => {
يمكن استخدام دوال الوحدة مباشرةً عبر المتغير المستورد noteService كما يلي:
const App = () => {
// ...
useEffect(() => {
// highlight-start
noteService
.getAll()
.then(response => {
setNotes(response.data)
})
// highlight-end
}, [])
const toggleImportanceOf = id => {
const note = notes.find(n => n.id === id)
const changedNote = { ...note, important: !note.important }
// highlight-start
noteService
.update(id, changedNote)
.then(response => {
setNotes(notes.map(note => note.id === id ? response.data : note))
})
// highlight-end
}
const addNote = (event) => {
event.preventDefault()
const noteObject = {
content: newNote,
important: Math.random() > 0.5
}
// highlight-start
noteService
.create(noteObject)
.then(response => {
setNotes(notes.concat(response.data))
setNewNote('')
})
// highlight-end
}
// ...
}
export default App
يمكننا أن نخطو بتنفيذنا خطوة إضافية. عندما يستخدم مكوّن App الدوال، فإنه يتلقى كائناً يحتوي على كامل استجابة طلب HTTP:
noteService
.getAll()
.then(response => {
setNotes(response.data)
})
لا يستخدم مكوّن App سوى خاصية response.data من كائن الاستجابة.
سيكون استخدام الوحدة أجمل بكثير إذا حصلنا على بيانات الاستجابة فقط بدلاً من استجابة HTTP كاملة. وسيبدو استخدام الوحدة حينها كالتالي:
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
}
لم نعد نعيد الـpromise الذي يعيده axios مباشرةً. بل نسند الـpromise إلى المتغير request ونستدعي طريقته then:
const getAll = () => {
const request = axios.get(baseUrl)
return request.then(response => response.data)
}
السطر الأخير في الدالة هو ببساطة تعبير أكثر إيجازاً عن الشيفرة نفسها كما هو موضح أدناه:
const getAll = () => {
const request = axios.get(baseUrl)
// highlight-start
return request.then(response => {
return response.data
})
// highlight-end
}
تظل دالة getAll المعدَّلة تعيد promise، لأن طريقة then الخاصة بالـpromise تعيد بدورها promise.
بعد تعريف معامل طريقة then ليعيد response.data مباشرةً، جعلنا دالة getAll تعمل كما أردنا. وعندما ينجح طلب HTTP، يعيد الـpromise البيانات المرسَلة في استجابة الواجهة الخلفية.
علينا تحديث مكوّن App ليعمل مع التغييرات التي أُجريت على وحدتنا. علينا إصلاح دوال الاستدعاء الراجعة المعطاة كمعاملات لطرق كائن noteService بحيث تستخدم بيانات الاستجابة المُعادة مباشرة:
const App = () => {
// ...
useEffect(() => {
noteService
.getAll()
// highlight-start
.then(initialNotes => {
setNotes(initialNotes)
// highlight-end
})
}, [])
const toggleImportanceOf = id => {
const note = notes.find(n => n.id === id)
const changedNote = { ...note, important: !note.important }
noteService
.update(id, changedNote)
// highlight-start
.then(returnedNote => {
setNotes(notes.map(note => note.id === id ? returnedNote : note))
// highlight-end
})
}
const addNote = (event) => {
event.preventDefault()
const noteObject = {
content: newNote,
important: Math.random() > 0.5
}
noteService
.create(noteObject)
// highlight-start
.then(returnedNote => {
setNotes(notes.concat(returnedNote))
// highlight-end
setNewNote('')
})
}
// ...
}
هذا كله معقّد بعض الشيء، ومحاولة شرحه قد تزيده صعوبة في الفهم فحسب. والإنترنت مليء بالمواد التي تناقش الموضوع، مثل هذه المادة.
يشرح كتاب «Async and performance» من سلسلة كتب You do not know JS الموضوع جيداً، لكن الشرح يمتد على صفحات عديدة.
تحتل الـpromises مكانة محورية في تطوير 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
}
لكن بما أن حقول الخصائص وأسماء المتغيرات في الكائن متطابقة، يكفي ببساطة كتابة ما يلي في JavaScript ES6:
const person = { name, age }
النتيجة متطابقة في كلا التعبيرين. فكلاهما ينشئ كائناً له خاصية name بالقيمة Leevi وخاصية age بالقيمة 0.
الـpromises والأخطاء
لو كان تطبيقنا يسمح للمستخدمين بحذف الملاحظات، فقد ننتهي إلى حالة يحاول فيها مستخدم تغيير أهمية ملاحظة حُذفت بالفعل من النظام.
لنحاكِ هذه الحالة بجعل دالة getAll في خدمة الملاحظات تعيد ملاحظة «مضمّنة في الشيفرة» (hardcoded) لا وجود لها فعلاً على الخادم الخلفي:
const getAll = () => {
const request = axios.get(baseUrl)
const nonExisting = {
id: 10000,
content: 'This note is not saved to server',
important: true,
}
return request.then(response => response.data.concat(nonExisting))
}
عندما نحاول تغيير أهمية الملاحظة المضمّنة في الشيفرة، نرى رسالة الخطأ التالية في وحدة التحكم. تقول الرسالة إن الخادم الخلفي استجاب لطلب HTTP PUT برمز الحالة 404 not found.

ينبغي أن يكون التطبيق قادراً على التعامل مع هذه الأنواع من حالات الخطأ بلطف. فلن يستطيع المستخدمون معرفة أن خطأً قد وقع إلا إذا صادف أن وحدة التحكم مفتوحة لديهم. والطريقة الوحيدة التي يمكن بها رؤية الخطأ في التطبيق هي أن النقر على الزر لا يؤثر على أهمية الملاحظة.
ذكرنا سابقاً أن الـpromise يمكن أن يكون في واحدة من ثلاث حالات مختلفة. وعندما يفشل طلب HTTP في axios، يصبح الـpromise المرتبط به مرفوضاً. ولا تتعامل شيفرتنا الحالية مع هذا الرفض بأي شكل.
تتم معالجة رفض الـpromise بتزويد طريقة then بدالة استدعاء راجعة ثانية تُستدعى في الحالة التي يُرفض فيها الـpromise.
لكن الطريقة الأكثر شيوعاً لإضافة معالج للـpromises المرفوضة هي استخدام طريقة catch.
عملياً، يُعرَّف معالج الأخطاء للـpromises المرفوضة هكذا:
axios
.get('http://example.com/probably_will_fail')
.then(response => {
console.log('success!')
})
.catch(error => {
console.log('fail')
})
إذا فشل الطلب، يُستدعى معالج الحدث المسجَّل بواسطة طريقة catch.
غالباً ما تُستخدم طريقة catch بوضعها في موضع أعمق داخل سلسلة الـpromises.
عند ربط عدة طرق .then معاً، ننشئ في الواقع سلسلة promises:
axios
.get('http://...')
.then(response => response.data)
.then(data => {
// ...
})
يمكن استخدام طريقة catch لتعريف دالة معالج في نهاية سلسلة promises، تُستدعى بمجرد أن يطرح أي promise في السلسلة خطأً ويصبح الـpromise مرفوضاً.
axios
.get('http://...')
.then(response => response.data)
.then(data => {
// ...
})
.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 ? returnedNote : note))
})
// highlight-start
.catch(error => {
alert(
`the note '${note.content}' was already deleted from server`
)
setNotes(notes.filter(n => n.id !== id))
})
// highlight-end
}
تُعرض رسالة الخطأ للمستخدم عبر نافذة الحوار alert القديمة الموثوقة، وتُستبعد الملاحظة المحذوفة من الحالة.
يتم إزالة ملاحظة محذوفة بالفعل من حالة التطبيق باستخدام طريقة filter في المصفوفة، وهي تعيد مصفوفة جديدة تتضمن فقط العناصر التي تعيد الدالة الممرَّرة كمعامل القيمة true من أجلها:
notes.filter(n => n.id !== id)
ربما ليست فكرة جيدة استخدام alert في تطبيقات React الأكثر جدية. وسنتعلم قريباً طريقة أكثر تقدماً لعرض الرسائل والإشعارات للمستخدمين. غير أن هناك حالات يمكن فيها لطريقة بسيطة ومجرَّبة مثل alert أن تعمل كنقطة انطلاق. ويمكن دائماً إضافة طريقة أكثر تقدماً لاحقاً، إذا توفر الوقت والطاقة لذلك.
يمكن العثور على شيفرة الحالة الحالية لتطبيقنا في الفرع part2-6 على GitHub.
قسم مطوّر Full stack
حان وقت التمارين مجدداً. يزداد تعقيد تطبيقنا الآن لأنه بالإضافة إلى الاعتناء بمكوّنات React في الواجهة الأمامية فقط، لدينا أيضاً واجهة خلفية تحفظ بيانات التطبيق.
وللتعامل مع التعقيد المتزايد، ينبغي أن نوسّع قسم مطوّر الويب ليصبح قسم مطوّر Full stack، الذي يذكّرنا بالتحقق من أن التواصل بين الواجهة الأمامية والواجهة الخلفية يحدث كما هو متوقع.
وهذا هو القسم المحدَّث:
تطوير Full stack صعب للغاية، ولهذا سأستخدم كل الوسائل الممكنة لتسهيله
- سأبقي وحدة تحكم مطوّر المتصفح مفتوحة طوال الوقت
- سأستخدم تبويب الشبكة في أدوات مطوّر المتصفح للتأكد من أن الواجهة الأمامية والواجهة الخلفية تتواصلان كما أتوقع
- سأراقب باستمرار حالة الخادم للتأكد من أن البيانات التي ترسلها الواجهة الأمامية إلى هناك تُحفظ فيه كما أتوقع
- سأتقدم بخطوات صغيرة
- سأكتب الكثير من عبارات console.log للتأكد من فهمي لكيفية تصرف الشيفرة وللمساعدة في تحديد المشكلات
- إذا لم تعمل شيفرتي، لن أكتب المزيد من الشيفرة. بل سأبدأ بحذف الشيفرة حتى تعمل، أو أعود إلى حالة كان فيها كل شيء يعمل
- عندما أطلب المساعدة في قناة الدورة على Discord أو في مكان آخر، سأصيغ أسئلتي بشكل سليم، انظر هنا لكيفية طلب المساعدة
تمارين 2.12.-2.15.
2.12: دفتر الهاتف، الخطوة 7
لنعد إلى تطبيق دفتر الهاتف.
حالياً، لا تُحفظ الأرقام التي تُضاف إلى دفتر الهاتف في خادم خلفي. أصلح هذا الوضع.
2.13: دفتر الهاتف، الخطوة 8
افصل الشيفرة التي تتعامل مع التواصل مع الواجهة الخلفية في وحدة خاصة بها باتباع المثال المعروض سابقاً في هذا الجزء من مادة الدورة.
2.14: دفتر الهاتف، الخطوة 9
اجعل من الممكن للمستخدمين حذف المدخلات من دفتر الهاتف. ويمكن أن يتم الحذف عبر زر مخصص لكل شخص في قائمة دفتر الهاتف. ويمكنك تأكيد الإجراء من المستخدم باستخدام طريقة window.confirm:

يمكن حذف المورد المرتبط بشخص في الواجهة الخلفية عبر إرسال طلب HTTP DELETE إلى عنوان URL الخاص بالمورد. فإذا كنا نحذف مثلاً شخصاً معرّفه id هو 2، فسيكون علينا إرسال طلب HTTP DELETE إلى العنوان localhost:3001/persons/2. ولا تُرسَل أي بيانات مع الطلب.
يمكنك إرسال طلب HTTP DELETE باستخدام مكتبة axios بالطريقة نفسها التي نرسل بها جميع الطلبات الأخرى.
ملاحظة: لا يمكنك استخدام الاسم delete لمتغير لأنه كلمة محجوزة في JavaScript. فمثلاً، ما يلي غير ممكن:
// استخدم اسماً آخر للمتغير!
const delete = (id) => {
// ...
}
2.15*: دفتر الهاتف، الخطوة 10
لماذا توجد نجمة في التمرين؟ انظر هنا للاطلاع على الشرح.
غيّر الوظيفة بحيث إذا أُضيف رقم لمستخدم موجود بالفعل، فسيحل الرقم الجديد محل الرقم القديم. ويوصى باستخدام طريقة HTTP PUT لتحديث رقم الهاتف.
إذا كانت معلومات الشخص موجودة بالفعل في دفتر الهاتف، فيمكن للتطبيق أن يطلب من المستخدم تأكيد الإجراء:
