النماذج
لنواصل توسيع تطبيقنا بالسماح للمستخدمين بإضافة ملاحظات جديدة. يمكنك العثور على شيفرة تطبيقنا الحالي هنا.
حفظ الملاحظات في حالة المكوّن
لكي تتحدّث صفحتنا عند إضافة ملاحظات جديدة، من الأفضل تخزين الملاحظات في حالة المكوّن App. لنستورد الدالة useState ونستخدمها لتعريف قطعة حالة تُهيَّأ بمصفوفة الملاحظات الأولية الممرَّرة في props.
import { useState } from 'react'
import Note from './components/Note'
const App = (props) => {
const [notes, setNotes] = useState(props.notes)
return (
<div>
<h1>Notes</h1>
<ul>
{notes.map(note =>
<Note key={note.id} note={note} />
)}
</ul>
</div>
)
}
export default App
يستخدم المكوّن الدالة useState لتهيئة قطعة الحالة المخزَّنة في notes بمصفوفة الملاحظات الممرَّرة في props:
const App = (props) => {
const [notes, setNotes] = useState(props.notes)
// ...
}
يمكننا أيضاً استخدام React Developer Tools لنرى أن هذا يحدث فعلاً:

إذا أردنا البدء بقائمة ملاحظات فارغة، فسنضبط القيمة الأولية كمصفوفة فارغة، ولأن props لن تُستخدم، يمكننا حذف الوسيط props من تعريف الدالة:
const App = () => {
const [notes, setNotes] = useState([])
// ...
}
لنبقَ على القيمة الأولية الممرَّرة في props في الوقت الحالي.
بعد ذلك، لنضف إلى المكوّن نموذج HTML يُستخدم لإضافة ملاحظات جديدة.
const App = (props) => {
const [notes, setNotes] = useState(props.notes)
// highlight-start
const addNote = (event) => {
event.preventDefault()
console.log('button clicked', event.target)
}
// highlight-end
return (
<div>
<h1>Notes</h1>
<ul>
{notes.map(note =>
<Note key={note.id} note={note} />
)}
</ul>
// highlight-start
<form onSubmit={addNote}>
<input />
<button type="submit">save</button>
</form>
// highlight-end
</div>
)
}
أضفنا الدالة addNote كمعالج أحداث إلى عنصر النموذج، وستُستدعى عند إرسال النموذج بالنقر على زر الإرسال.
نستخدم الطريقة التي ناقشناها في الجزء 1 لتعريف معالج الأحداث:
const addNote = (event) => {
event.preventDefault()
console.log('button clicked', event.target)
}
الوسيط event هو الحدث الذي يُطلق استدعاء دالة معالج الأحداث:
يستدعي معالج الأحداث فوراً الدالة event.preventDefault()، التي تمنع الإجراء الافتراضي لإرسال النموذج. وكان الإجراء الافتراضي سيؤدي، من بين أمور أخرى، إلى إعادة تحميل الصفحة.
يُسجَّل هدف الحدث المخزَّن في event.target في وحدة التحكم:

الهدف في هذه الحالة هو النموذج الذي عرّفناه في مكوّننا.
كيف نصل إلى البيانات الموجودة في عنصر input الخاص بالنموذج؟
المكوّن المتحكَّم به
هناك طرق عديدة لتحقيق ذلك؛ أول طريقة سنلقي عليها نظرة هي استخدام ما يسمى المكوّنات المتحكَّم بها (controlled components).
لنضف قطعة حالة جديدة تسمى newNote لتخزين إدخال المستخدم ولنضبطها كخاصية value لعنصر input:
const App = (props) => {
const [notes, setNotes] = useState(props.notes)
// highlight-start
const [newNote, setNewNote] = useState(
'a new note...'
)
// highlight-end
const addNote = (event) => {
event.preventDefault()
console.log('button clicked', event.target)
}
return (
<div>
<h1>Notes</h1>
<ul>
{notes.map(note =>
<Note key={note.id} note={note} />
)}
</ul>
<form onSubmit={addNote}>
<input value={newNote} />
<button type="submit">save</button>
</form>
</div>
)
}
يظهر النص البديل المخزَّن كقيمة أولية للحالة newNote في عنصر input، لكن نص الإدخال لا يمكن تحريره. وتعرض وحدة التحكم تحذيراً يعطينا فكرة عما قد يكون الخطأ:

بما أننا أسندنا قطعة من حالة المكوّن App كخاصية value لعنصر الإدخال، فإن المكوّن App الآن يتحكم في سلوك عنصر الإدخال.
لتمكين تحرير عنصر الإدخال، علينا تسجيل معالج أحداث يزامن التغييرات التي تطرأ على الإدخال مع حالة المكوّن:
const App = (props) => {
const [notes, setNotes] = useState(props.notes)
const [newNote, setNewNote] = useState(
'a new note...'
)
// ...
// highlight-start
const handleNoteChange = (event) => {
console.log(event.target.value)
setNewNote(event.target.value)
}
// highlight-end
return (
<div>
<h1>Notes</h1>
<ul>
{notes.map(note =>
<Note key={note.id} note={note} />
)}
</ul>
<form onSubmit={addNote}>
<input
value={newNote}
onChange={handleNoteChange}
/>
<button type="submit">save</button>
</form>
</div>
)
}
سجّلنا الآن معالج أحداث على الخاصية onChange لعنصر input في النموذج:
<input
value={newNote}
onChange={handleNoteChange}
/>
يُستدعى معالج الأحداث في كل مرة يحدث فيها تغيير في عنصر الإدخال. وتستقبل دالة معالج الأحداث كائن الحدث كوسيط event لها:
const handleNoteChange = (event) => {
console.log(event.target.value)
setNewNote(event.target.value)
}
تشير خاصية target في كائن الحدث الآن إلى عنصر input المتحكَّم به، ويعبّر event.target.value عن قيمة الإدخال في ذلك العنصر.
لاحظ أننا لم نحتج إلى استدعاء الدالة event.preventDefault() كما فعلنا في معالج الأحداث onSubmit. ذلك لأن أي إجراء افتراضي لا يحدث عند تغيير الإدخال، بخلاف إرسال النموذج.
يمكنك المتابعة في وحدة التحكم لترى كيف يُستدعى معالج الأحداث:

هل تذكّرت تثبيت React devtools، أليس كذلك؟ جيد. يمكنك مشاهدة كيفية تغيّر الحالة مباشرةً من تبويب React Devtools:

الآن تعكس حالة newNote في المكوّن App القيمة الحالية للإدخال، ما يعني أننا نستطيع إكمال الدالة addNote لإنشاء ملاحظات جديدة:
const addNote = (event) => {
event.preventDefault()
const noteObject = {
content: newNote,
important: Math.random() < 0.5,
id: String(notes.length + 1),
}
setNotes(notes.concat(noteObject))
setNewNote('')
}
أولاً، ننشئ كائناً جديداً للملاحظة يسمى noteObject يستقبل محتواه من الحالة newNote في المكوّن. ويُولَّد المعرّف الفريد id بناءً على العدد الإجمالي للملاحظات. هذه الطريقة تصلح لتطبيقنا لأن الملاحظات لا تُحذف أبداً. وبمساعدة الدالة Math.random()، يكون لملاحظتنا احتمال 50% أن تُعلَّم كمهمة.
تُضاف الملاحظة الجديدة إلى قائمة الملاحظات باستخدام دالة المصفوفة concat، التي قدّمناها في الجزء 1:
setNotes(notes.concat(noteObject))
لا تعدّل الدالة مصفوفة notes الأصلية، بل تنشئ نسخة جديدة من المصفوفة مع إضافة العنصر الجديد إلى نهايتها. هذا مهم لأننا يجب أن لا نعدّل الحالة مباشرة أبداً في React!
يعيد معالج الأحداث أيضاً ضبط قيمة عنصر الإدخال المتحكَّم به باستدعاء الدالة setNewNote الخاصة بالحالة newNote:
setNewNote('')
يمكنك العثور على شيفرة تطبيقنا الحالي كاملةً في الفرع part2-2 من مستودع GitHub هذا.
تصفية العناصر المعروضة
لنضف بعض الوظائف الجديدة إلى تطبيقنا تتيح لنا عرض الملاحظات المهمة فقط.
لنضف قطعة حالة إلى المكوّن App تتبّع الملاحظات التي ينبغي عرضها:
const App = (props) => {
const [notes, setNotes] = useState(props.notes)
const [newNote, setNewNote] = useState('')
const [showAll, setShowAll] = useState(true)
// ...
}
لنغيّر المكوّن بحيث يخزّن قائمة بكل الملاحظات المعروضة في المتغير notesToShow. وتعتمد عناصر القائمة على حالة المكوّن:
import { useState } from 'react'
import Note from './components/Note'
const App = (props) => {
const [notes, setNotes] = useState(props.notes)
const [newNote, setNewNote] = useState('')
const [showAll, setShowAll] = useState(true)
// ...
// highlight-start
const notesToShow = showAll
? notes
: notes.filter(note => note.important === true)
// highlight-end
return (
<div>
<h1>Notes</h1>
<ul>
{notesToShow.map(note =>
<Note key={note.id} note={note} />
)}
</ul>
// ...
</div>
)
}
تعريف المتغير notesToShow مختصر جداً:
const notesToShow = showAll
? notes
: notes.filter(note => note.important === true)
يستخدم التعريف المعامل الشرطي الموجود أيضاً في كثير من لغات البرمجة الأخرى.
يعمل المعامل كما يلي. إذا كان لدينا:
const result = condition ? val1 : val2
فستُضبط قيمة المتغير result على قيمة val1 إذا كانت condition صحيحة. وإذا كانت condition خاطئة، فستُضبط قيمة المتغير result على قيمة val2.
إذا كانت قيمة showAll خاطئة، فسيُسند إلى المتغير notesToShow نص بقائمة تحتوي فقط على الملاحظات التي تكون خاصية important فيها مضبوطة على true. وتُنجز التصفية بمساعدة دالة المصفوفة filter:
notes.filter(note => note.important === true)
معامل المقارنة زائد عن الحاجة، لأن قيمة note.important هي إما true أو false، ما يعني أنه يمكننا ببساطة كتابة:
notes.filter(note => note.important)
أظهرنا معامل المقارنة أولاً للتأكيد على تفصيل مهم: في JavaScript لا يعمل val1 == val2 دائماً كما هو متوقع. لذلك من الأكثر أماناً عند إجراء المقارنات استخدام val1 === val2 حصراً. يمكنك القراءة أكثر عن الموضوع هنا.
يمكنك تجربة وظيفة التصفية بتغيير القيمة الأولية للحالة showAll.
بعد ذلك، لنضف وظيفة تتيح للمستخدمين تبديل الحالة showAll للتطبيق من واجهة المستخدم.
التغييرات ذات الصلة معروضة أدناه:
import { useState } from 'react'
import Note from './components/Note'
const App = (props) => {
const [notes, setNotes] = useState(props.notes)
const [newNote, setNewNote] = useState('')
const [showAll, setShowAll] = useState(true)
// ...
return (
<div>
<h1>Notes</h1>
// highlight-start
<div>
<button onClick={() => setShowAll(!showAll)}>
show {showAll ? 'important' : 'all'}
</button>
</div>
// highlight-end
<ul>
{notesToShow.map(note =>
<Note key={note.id} note={note} />
)}
</ul>
// ...
</div>
)
}
تتحكم الملاحظات المعروضة (الكل مقابل المهمة) بزر. ومعالج الأحداث الخاص بالزر بسيط لدرجة أنه عُرّف مباشرة في خاصية عنصر الزر. يبدّل معالج الأحداث قيمة showAll من true إلى false والعكس:
() => setShowAll(!showAll)
يعتمد نص الزر على قيمة الحالة showAll:
show {showAll ? 'important' : 'all'}
يمكنك العثور على شيفرة تطبيقنا الحالي كاملةً في الفرع part2-3 من مستودع GitHub هذا.
تمارين 2.6.-2.10.
في التمرين الأول، سنبدأ العمل على تطبيق سيُطوَّر أكثر في التمارين اللاحقة. في مجموعات التمارين المترابطة، يكفي إعادة النسخة النهائية من تطبيقك. ويمكنك أيضاً عمل commit منفصل بعد إنهاء كل جزء من مجموعة التمارين، لكن ذلك غير مطلوب.
2.6: دفتر الهاتف الخطوة 1
لننشئ دفتر هاتف بسيطاً. في هذا الجزء، سنضيف أسماءً فقط إلى دفتر الهاتف.
لنبدأ بتنفيذ إضافة شخص إلى دفتر الهاتف.
يمكنك استخدام الشيفرة أدناه كنقطة انطلاق للمكوّن App في تطبيقك:
import { useState } from 'react'
const App = () => {
const [persons, setPersons] = useState([
{ name: 'Arto Hellas' }
])
const [newName, setNewName] = useState('')
return (
<div>
<h2>Phonebook</h2>
<form>
<div>
name: <input />
</div>
<div>
<button type="submit">add</button>
</div>
</form>
<h2>Numbers</h2>
...
</div>
)
}
export default App
الحالة newName مخصصة للتحكم بعنصر الإدخال في النموذج.
أحياناً يكون من المفيد عرض الحالة والمتغيرات الأخرى كنص لأغراض تصحيح الأخطاء. يمكنك مؤقتاً إضافة العنصر التالي إلى المكوّن المعروض:
<div>debug: {newName}</div>
من المهم أيضاً الاستفادة جيداً مما تعلمناه في فصل تصحيح أخطاء تطبيقات React من الجزء الأول. إن إضافة React developer tools مفيدة للغاية لتتبع التغييرات التي تطرأ على حالة التطبيق.
بعد إنهاء هذا التمرين، ينبغي أن يبدو تطبيقك شيئاً مثل هذا:

لاحظ استخدام إضافة React developer tools في الصورة أعلاه!
ملاحظة:
- يمكنك استخدام اسم الشخص كقيمة لخاصية key
- تذكّر منع الإجراء الافتراضي لإرسال نماذج HTML!
2.7: دفتر الهاتف الخطوة 2
امنع المستخدم من إضافة أسماء موجودة مسبقاً في دفتر الهاتف. تمتلك مصفوفات JavaScript دوال methods عديدة مناسبة لإنجاز هذه المهمة. ضع في اعتبارك كيفية عمل مساواة الكائنات في JavaScript.
أصدر تحذيراً بالأمر alert عند محاولة كهذه:

تلميح: عندما تشكّل نصوصاً تحتوي قيماً من متغيرات، يُوصى باستخدام نص قالب (template string):
`${newName} is already added to phonebook`
إذا كان المتغير newName يحمل القيمة Arto Hellas، فسيعيد تعبير نص القالب النص
`Arto Hellas is already added to phonebook`
ويمكن فعل الشيء نفسه على طريقة Java أكثر باستخدام معامل الجمع:
newName + ' is already added to phonebook'
استخدام نصوص القوالب هو الخيار الأكثر أصالةً وعلامة على مبرمج JavaScript حقيقي.
2.8: دفتر الهاتف الخطوة 3
وسّع تطبيقك بالسماح للمستخدمين بإضافة أرقام هواتف إلى دفتر الهاتف. ستحتاج إلى إضافة عنصر input ثانٍ إلى النموذج (مع معالج أحداث خاص به):
<form>
<div>name: <input /></div>
<div>number: <input /></div>
<div><button type="submit">add</button></div>
</form>
عند هذه النقطة، قد يبدو التطبيق شيئاً مثل هذا. تعرض الصورة أيضاً حالة التطبيق بمساعدة React developer tools:

2.9*: دفتر الهاتف الخطوة 4
نفّذ حقل بحث يمكن استخدامه لتصفية قائمة الأشخاص بالاسم:

يمكنك تنفيذ حقل البحث كعنصر input يوضع خارج نموذج HTML. منطق التصفية الظاهر في الصورة غير حساس لحالة الأحرف، أي أن مصطلح البحث arto يعيد أيضاً نتائج تحتوي على Arto بحرف A كبير.
ملاحظة: عند العمل على وظيفة جديدة، غالباً ما يكون من المفيد «تثبيت» بعض البيانات الوهمية مباشرة في تطبيقك، مثل
const App = () => {
const [persons, setPersons] = useState([
{ name: 'Arto Hellas', number: '040-123456', id: 1 },
{ name: 'Ada Lovelace', number: '39-44-5323523', id: 2 },
{ name: 'Dan Abramov', number: '12-43-234345', id: 3 },
{ name: 'Mary Poppendieck', number: '39-23-6423122', id: 4 }
])
// ...
}
يوفر عليك هذا عناء إدخال البيانات يدوياً في تطبيقك لتجربة وظيفتك الجديدة.
2.10: دفتر الهاتف الخطوة 5
إذا نفّذت تطبيقك في مكوّن واحد، فأعد هيكلته باستخراج أجزاء مناسبة إلى مكوّنات جديدة. أبقِ حالة التطبيق وكل معالجات الأحداث في المكوّن الجذري App.
يكفي استخراج ثلاثة مكوّنات من التطبيق. ومن المرشحين الجيدين كمكوّنات منفصلة، مثلاً، مرشّح البحث، ونموذج إضافة أشخاص جدد إلى دفتر الهاتف، ومكوّن يعرض كل الأشخاص من دفتر الهاتف، ومكوّن يعرض تفاصيل شخص واحد.
قد يبدو المكوّن الجذري للتطبيق مشابهاً لهذا بعد إعادة الهيكلة. المكوّن الجذري المعاد هيكلته أدناه يعرض العناوين فقط ويترك للمكوّنات المستخرجة الاهتمام بالباقي.
const App = () => {
// ...
return (
<div>
<h2>Phonebook</h2>
<Filter ... />
<h3>Add a new</h3>
<PersonForm
...
/>
<h3>Numbers</h3>
<Persons ... />
</div>
)
}
ملاحظة: قد تواجه مشكلات في هذا التمرين إذا عرّفت مكوّناتك «في المكان الخاطئ». والآن وقت مناسب لمراجعة فصل لا تعرّف مكوّناً داخل مكوّن آخر من الجزء السابق.