إضافة الأنماط إلى تطبيق React
مظهر تطبيق Notes الحالي لدينا متواضع تماماً. في التمرين 0.2، كان المطلوب الاطلاع على درس CSS من Mozilla.
لنلقِ نظرة على كيفية إضافة أنماط إلى تطبيق React. هناك عدة طرق مختلفة لفعل ذلك، وسنلقي نظرة على الطرق الأخرى لاحقاً. أولاً، سنضيف CSS إلى تطبيقنا بالطريقة القديمة؛ في ملف واحد دون استخدام معالج CSS مسبق (رغم أن هذا ليس صحيحاً تماماً كما سنتعلم لاحقاً).
لنضف ملفاً جديداً index.css تحت مجلد src ثم نضيفه إلى التطبيق باستيراده في الملف main.jsx:
import './index.css'
لنضف قاعدة CSS التالية إلى الملف index.css:
h1 {
color: green;
}
تتكوّن قواعد CSS من محدّدات وتعريفات. يحدّد المحدّد العناصر التي يجب تطبيق القاعدة عليها. المحدّد أعلاه هو h1، وسيطابق جميع وسوم العناوين h1 في تطبيقنا.
يعيّن التعريف خاصية color على القيمة green.
يمكن أن تحتوي قاعدة CSS واحدة على عدد غير محدود من الخصائص. لنعدّل القاعدة السابقة لجعل النص مائلاً، بتعريف نمط الخط كـitalic:
h1 {
color: green;
font-style: italic;
}
هناك طرق عديدة لمطابقة العناصر باستخدام أنواع مختلفة من محدّدات CSS.
لو أردنا، على سبيل المثال، استهداف كل واحدة من الملاحظات بأنماطنا، لأمكننا استخدام المحدّد li، لأن جميع الملاحظات ملفوفة داخل وسوم li:
const Note = ({ note, toggleImportance }) => {
const label = note.important
? 'make not important'
: 'make important'
return (
<li>
{note.content}
<button onClick={toggleImportance}>{label}</button>
</li>
)
}
لنضف القاعدة التالية إلى ورقة الأنماط لدينا (بما أن معرفتي بتصميم الويب الأنيق تقترب من الصفر، فالأنماط لا معنى لها تقريباً):
li {
color: grey;
padding-top: 3px;
font-size: 15px;
}
استخدام أنواع العناصر لتعريف قواعد CSS مشكِل بعض الشيء. إذا احتوى تطبيقنا على وسوم li أخرى، فستُطبَّق عليها قاعدة النمط نفسها أيضاً.
إذا أردنا تطبيق نمطنا على الملاحظات تحديداً، فمن الأفضل استخدام محدّدات الأصناف.
في HTML العادي، تُعرَّف الأصناف كقيمة للخاصية class:
<li class="note">some text...</li>
في React علينا استخدام خاصية className بدلاً من خاصية class. مع أخذ ذلك في الاعتبار، لنُجرِ التغييرات التالية على مكوّن Note:
const Note = ({ note, toggleImportance }) => {
const label = note.important
? 'make not important'
: 'make important'
return (
<li className='note'>
{note.content}
<button onClick={toggleImportance}>{label}</button>
</li>
)
}
تُعرَّف محدّدات الأصناف بالصيغة .classname:
.note {
color: grey;
padding-top: 5px;
font-size: 15px;
}
إذا أضفت الآن عناصر li أخرى إلى التطبيق، فلن تتأثر بقاعدة النمط أعلاه.
رسالة خطأ محسّنة
نفّذنا سابقاً رسالة الخطأ التي كانت تُعرض عندما يحاول المستخدم تبديل أهمية ملاحظة محذوفة باستخدام الدالة alert. لننفّذ رسالة الخطأ كمكوّن React خاص بها في الملف src/components/Notification.jsx.
المكوّن بسيط جداً:
const Notification = ({ message }) => {
if (message === null) {
return null
}
return (
<div className="error">
{message}
</div>
)
}
export default Notification
إذا كانت قيمة prop message هي null، فلا يُعرض شيء على الشاشة، وفي الحالات الأخرى تُعرض الرسالة داخل عنصر div.
لنضف قطعة حالة جديدة تُسمى errorMessage إلى مكوّن App. لنهيّئها برسالة خطأ ما حتى نتمكن من اختبار مكوّننا فوراً:
import { useState, useEffect } from 'react'
import Note from './components/Note'
import noteService from './services/notes'
import Notification from './components/Notification'
const App = () => {
const [notes, setNotes] = useState([])
const [newNote, setNewNote] = useState('')
const [showAll, setShowAll] = useState(true)
const [errorMessage, setErrorMessage] = useState('some error happened...')
// ...
return (
<div>
<h1>Notes</h1>
<Notification message={errorMessage} />
<div>
<button onClick={() => setShowAll(!showAll)}>
show {showAll ? 'important' : 'all' }
</button>
</div>
// ...
</div>
)
}
ثم لنضف قاعدة نمط تناسب رسالة خطأ:
.error {
color: red;
background: lightgrey;
font-size: 20px;
border-style: solid;
border-radius: 5px;
padding: 10px;
margin-bottom: 10px;
}
الآن أصبحنا مستعدين لإضافة المنطق الخاص بعرض رسالة الخطأ. لنغيّر الدالة toggleImportanceOf على النحو التالي:
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 => {
// highlight-start
setErrorMessage(
`Note '${note.content}' was already removed from server`
)
setTimeout(() => {
setErrorMessage(null)
}, 5000)
// highlight-end
setNotes(notes.filter(n => n.id !== id))
})
}
عند حدوث الخطأ نضيف رسالة خطأ وصفية إلى حالة errorMessage. وفي الوقت نفسه نبدأ مؤقتاً يعيّن حالة errorMessage إلى null بعد خمس ثوانٍ.
تبدو النتيجة هكذا:

يمكن العثور على شيفرة الحالة الحالية لتطبيقنا في الفرع part2-7 على GitHub.
الأنماط المضمّنة
يتيح React أيضاً كتابة الأنماط مباشرة في الشيفرة بما يُسمى الأنماط المضمّنة.
الفكرة وراء تعريف الأنماط المضمّنة بسيطة للغاية. يمكن تزويد أي مكوّن أو عنصر React بمجموعة من خصائص CSS ككائن JavaScript عبر خاصية style.
تُعرَّف قواعد CSS في JavaScript بشكل مختلف قليلاً عمّا هي عليه في ملفات CSS العادية. لنفترض أننا أردنا إعطاء بعض العناصر اللون الأخضر وخطاً مائلاً. في CSS سيبدو الأمر هكذا:
{
color: green;
font-style: italic;
}
لكن ككائن نمط مضمّن في React سيبدو هكذا:
{
color: 'green',
fontStyle: 'italic'
}
تُعرَّف كل خاصية CSS كخاصية منفصلة في كائن JavaScript. ويمكن ببساطة تعريف القيم الرقمية للبكسلات كأعداد صحيحة. ومن أبرز الفروق مقارنة بـCSS العادي أن خصائص CSS الموصولة بشرطات (kebab case) تُكتب بصيغة camelCase.
لنضف مكوّن تذييل، Footer، إلى تطبيقنا ونعرّف له أنماطاً مضمّنة. يُعرَّف المكوّن في الملف components/Footer.jsx ويُستخدم في الملف App.jsx كما يلي:
const Footer = () => {
const footerStyle = {
color: 'green',
fontStyle: 'italic'
}
return (
<div style={footerStyle}>
<br />
<p>
Note app, Department of Computer Science, University of Helsinki 2025
</p>
</div>
)
}
export default Footer
import { useState, useEffect } from 'react'
import Footer from './components/Footer'
import Note from './components/Note'
import Notification from './components/Notification'
import noteService from './services/notes'
const App = () => {
// ...
return (
<div>
<h1>Notes</h1>
<Notification message={errorMessage} />
// ...
<Footer />
</div>
)
}
تأتي الأنماط المضمّنة بقيود معينة. فعلى سبيل المثال، لا يمكن استخدام ما يُسمى الحالات الزائفة (pseudo-classes) بشكل مباشر.
تتعارض الأنماط المضمّنة وبعض الطرق الأخرى لإضافة الأنماط إلى مكوّنات React تماماً مع الأعراف القديمة. فتقليدياً، كان يُعتبر من أفضل الممارسات الفصل التام بين CSS والمحتوى (HTML) والوظائف (JavaScript). ووفقاً لهذه المدرسة الأقدم في التفكير، كان الهدف كتابة CSS وHTML وJavaScript في ملفاتها المنفصلة.
فلسفة React في الواقع هي النقيض التام لهذا. ولأن فصل CSS وHTML وJavaScript في ملفات منفصلة لم يبدُ قابلاً للتوسع جيداً في التطبيقات الأكبر، يبني React تقسيم التطبيق على أساس كياناته الوظيفية المنطقية.
الوحدات البنيوية التي تتكوّن منها الكيانات الوظيفية للتطبيق هي مكوّنات React. يعرّف مكوّن React الـHTML اللازم لهيكلة المحتوى، ودوال JavaScript اللازمة لتحديد الوظائف، وكذلك أنماط المكوّن؛ كل ذلك في مكان واحد. والهدف من ذلك إنشاء مكوّنات فردية مستقلة وقابلة لإعادة الاستخدام قدر الإمكان.
يمكن العثور على شيفرة النسخة النهائية لتطبيقنا في الفرع part2-8 على GitHub.
تمارين 2.16.-2.17.
2.16: دليل الهاتف الخطوة 11
استخدم مثال رسالة الخطأ المحسّنة من الجزء 2 كدليل لعرض إشعار يستمر بضع ثوانٍ بعد تنفيذ عملية ناجحة (إضافة شخص أو تغيير رقم):

2.17*: دليل الهاتف الخطوة 12
افتح تطبيقك في متصفحين. إذا حذفت شخصاً في المتصفح 1 قبل فترة وجيزة من محاولة تغيير رقم هاتف الشخص في المتصفح 2، فستحصل على رسائل الخطأ التالية:

أصلح المشكلة وفقاً للمثال المعروض في الوعود والأخطاء في الجزء 2. عدّل المثال بحيث تُعرض للمستخدم رسالة عندما لا تنجح العملية. يجب أن تبدو الرسائل المعروضة للأحداث الناجحة وغير الناجحة مختلفة:

ملاحظة أنه حتى لو تعاملت مع الاستثناء، فستظل رسالة الخطأ الأولى "404" تُطبع في وحدة التحكم. لكن يجب ألا ترى "Uncaught (in promise) Error".
بضع ملاحظات مهمة
في نهاية هذا الجزء هناك بضعة تمارين أكثر تحدياً. في هذه المرحلة، يمكنك تخطي التمارين إذا كانت مرهقة أكثر من اللازم، وسنعود إلى الموضوعات نفسها لاحقاً. وتستحق المادة القراءة في كل الأحوال.
فعلنا شيئاً واحداً في تطبيقنا يخفي مصدراً نموذجياً جداً للأخطاء.
عيّنّا الحالة notes بقيمة أولية هي مصفوفة فارغة:
const App = () => {
const [notes, setNotes] = useState([])
// ...
}
هذه قيمة أولية طبيعية جداً لأن الملاحظات مجموعة، أي أن هناك ملاحظات كثيرة ستخزّنها الحالة.
لو كانت الحالة تخزّن "شيئاً واحداً" فقط، لكانت القيمة الأولية الأنسب هي null للدلالة على أنه لا يوجد شيء في الحالة في البداية. لنرَ ما يحدث إذا استخدمنا هذه القيمة الأولية:
const App = () => {
const [notes, setNotes] = useState(null)
// ...
}
ينهار التطبيق:

تعطي رسالة الخطأ سبب الخطأ وموقعه. الشيفرة التي تسبّبت في المشكلات هي التالية:
// تحصل notesToShow على قيمة notes
const notesToShow = showAll
? notes
: notes.filter(note => note.important)
// ...
{notesToShow.map(note =>
<Note key={note.id} note={note} />
)}
رسالة الخطأ هي
Cannot read properties of null (reading 'map')
يُسند إلى المتغير notesToShow أولاً قيمة الحالة notes ثم تحاول الشيفرة استدعاء الدالة map على كائن غير موجود، أي على null.
ما السبب في ذلك؟
يستخدم خطاف التأثير الدالة setNotes لتعيين notes إلى الملاحظات التي تعيدها الواجهة الخلفية:
useEffect(() => {
noteService
.getAll()
.then(initialNotes => {
setNotes(initialNotes)
})
}, [])
لكن المشكلة أن التأثير لا يُنفَّذ إلا بعد العرض الأول. ولأن قيمة notes الأولية هي null:
const App = () => {
const [notes, setNotes] = useState(null)
// ...
ففي العرض الأول تُنفَّذ الشيفرة التالية:
notesToShow = notes
// ...
notesToShow.map(note => ...)
وهذا يفجّر التطبيق لأننا لا نستطيع استدعاء الدالة map على القيمة null.
عندما نعيّن notes لتكون في البداية مصفوفة فارغة، لا يحدث خطأ لأنه يُسمح باستدعاء map على مصفوفة فارغة.
إذن، فقد "أخفت" تهيئة الحالة المشكلة الناتجة عن أن البيانات لم تُجلب بعد من الواجهة الخلفية.
طريقة أخرى للتحايل على المشكلة هي استخدام العرض الشرطي وإرجاع null إذا لم تكن حالة المكوّن مهيّأة بشكل صحيح:
const App = () => {
const [notes, setNotes] = useState(null)
// ...
useEffect(() => {
noteService
.getAll()
.then(initialNotes => {
setNotes(initialNotes)
})
}, [])
// لا تعرض أي شيء إذا كانت notes ما زالت null
// highlight-start
if (!notes) {
return null
}
// highlight-end
// ...
}
إذن في العرض الأول لا يُعرض شيء. وعندما تصل الملاحظات من الواجهة الخلفية، استخدم التأثير الدالة setNotes لتعيين قيمة الحالة notes. وهذا يؤدي إلى إعادة عرض المكوّن، وفي العرض الثاني تُعرض الملاحظات على الشاشة.
الطريقة القائمة على العرض الشرطي مناسبة في الحالات التي يستحيل فيها تعريف الحالة بحيث يكون العرض الأول ممكناً.
الأمر الآخر الذي ما زلنا بحاجة إلى إلقاء نظرة أدق عليه هو المعامل الثاني لـuseEffect:
useEffect(() => {
noteService
.getAll()
.then(initialNotes => {
setNotes(initialNotes)
})
}, [])
يُستخدم المعامل الثاني لـuseEffect لتحديد عدد مرات تنفيذ التأثير. والمبدأ هو أن التأثير يُنفَّذ دائماً بعد العرض الأول للمكوّن وعندما تتغيّر قيمة المعامل الثاني.
إذا كان المعامل الثاني مصفوفة فارغة []، فلن يتغيّر محتواها أبداً ولن يُنفَّذ التأثير إلا بعد العرض الأول للمكوّن. وهذا بالضبط ما نريده عندما نهيّئ حالة التطبيق من الخادم.
ومع ذلك، هناك حالات نريد فيها تنفيذ التأثير في أوقات أخرى، مثل عندما تتغيّر حالة المكوّن بطريقة معينة.
تأمّل التطبيق البسيط التالي للاستعلام عن أسعار صرف العملات من واجهة أسعار الصرف:
import { useState, useEffect } from 'react'
import axios from 'axios'
const App = () => {
const [value, setValue] = useState('')
const [rates, setRates] = useState({})
const [currency, setCurrency] = useState(null)
useEffect(() => {
console.log('effect run, currency is now', currency)
// تخطَّ إذا لم تكن العملة معرّفة
if (currency) {
console.log('fetching exchange rates...')
axios
.get(`https://open.er-api.com/v6/latest/${currency}`)
.then(response => {
setRates(response.data.rates)
})
}
}, [currency])
const handleChange = (event) => {
setValue(event.target.value)
}
const onSearch = (event) => {
event.preventDefault()
setCurrency(value)
}
return (
<div>
<form onSubmit={onSearch}>
currency: <input value={value} onChange={handleChange} />
<button type="submit">exchange rate</button>
</form>
<pre>
{JSON.stringify(rates, null, 2)}
</pre>
</div>
)
}
export default App
تحتوي واجهة مستخدم التطبيق على نموذج يُكتب في حقل إدخاله اسم العملة المطلوبة. وإذا كانت العملة موجودة، يعرض التطبيق أسعار صرفها مقابل العملات الأخرى:

يعيّن التطبيق اسم العملة المُدخَل في النموذج إلى الحالة currency في لحظة الضغط على الزر.
وعندما تحصل currency على قيمة جديدة، يجلب التطبيق أسعار صرفها من الـAPI في دالة التأثير:
const App = () => {
// ...
const [currency, setCurrency] = useState(null)
useEffect(() => {
console.log('effect run, currency is now', currency)
// تخطَّ إذا لم تكن العملة معرّفة
if (currency) {
console.log('fetching exchange rates...')
axios
.get(`https://open.er-api.com/v6/latest/${currency}`)
.then(response => {
setRates(response.data.rates)
})
}
}, [currency])
// ...
}
أصبح لخطاف useEffect الآن [currency] كمعامل ثانٍ. لذلك تُنفَّذ دالة التأثير بعد العرض الأول، ودائماً عندما تتغيّر مصفوفة الاعتماديات [currency] بوصفها معامله الثاني. أي أنه عندما تحصل الحالة currency على قيمة جديدة، يتغيّر محتوى مصفوفة الاعتماديات وتُنفَّذ دالة التأثير.
من الطبيعي اختيار null كقيمة أولية للمتغير currency، لأن currency يمثل عنصراً واحداً. وتشير القيمة الأولية null إلى أنه لا يوجد شيء في الحالة بعد، كما يسهل التحقق بجملة if بسيطة مما إذا كانت قيمة قد أُسندت إلى المتغير. وللتأثير الشرط التالي
if (currency) {
// تُجلب أسعار الصرف
}
وهو ما يمنع طلب أسعار الصرف مباشرة بعد العرض الأول عندما لا يزال المتغير currency يحمل القيمة الأولية، أي قيمة null.
فإذا كتب المستخدم مثلاً eur في حقل البحث، يستخدم التطبيق Axios لتنفيذ طلب HTTP GET إلى العنوان https://open.er-api.com/v6/latest/eur ويخزّن الاستجابة في حالة rates.
وعندما يُدخل المستخدم بعد ذلك قيمة أخرى في حقل البحث، مثلاً usd، تُنفَّذ دالة التأثير مرة أخرى وتُطلب أسعار صرف العملة الجديدة من الـAPI.
قد تبدو الطريقة المعروضة هنا لتنفيذ طلبات API غريبة بعض الشيء. كان يمكن بناء هذا التطبيق تحديداً دون استخدام useEffect إطلاقاً، بتنفيذ طلبات API مباشرة في دالة معالج إرسال النموذج:
const onSearch = (event) => {
event.preventDefault()
axios
.get(`https://open.er-api.com/v6/latest/${value}`)
.then(response => {
setRates(response.data.rates)
})
}
ومع ذلك، هناك حالات لا تنجح فيها تلك التقنية. فعلى سبيل المثال، قد تصادف إحدى هذه الحالات في التمرين 2.20 حيث قد يوفّر استخدام useEffect حلاً. لاحظ أن هذا يعتمد إلى حد كبير على النهج الذي اخترته، فمثلاً لا يستخدم الحل النموذجي هذه الحيلة.
تمارين 2.18.-2.20.
2.18* بيانات الدول، الخطوة 1
على https://studies.cs.helsinki.fi/restcountries/ يمكنك العثور على خدمة تقدّم الكثير من المعلومات المتعلقة بمختلف الدول بصيغة تُسمى قابلة للقراءة آلياً عبر REST API. اصنع تطبيقاً يتيح لك عرض معلومات من دول مختلفة.
واجهة المستخدم بسيطة جداً. تُعثر على الدولة المطلوب عرضها بكتابة استعلام بحث في حقل البحث.
إذا كانت هناك دول كثيرة جداً (أكثر من 10) تطابق الاستعلام، فيُطلب من المستخدم جعل استعلامه أكثر تحديداً:

إذا كانت هناك عشر دول أو أقل، ولكن أكثر من واحدة، فتُعرض جميع الدول المطابقة للاستعلام:

وعندما تكون هناك دولة واحدة فقط مطابقة للاستعلام، تُعرض البيانات الأساسية للدولة (مثل العاصمة والمساحة) وعلمها واللغات المتحدث بها:

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

في هذا التمرين أيضاً، يكفي أن يعمل تطبيقك مع معظم الدول. ويمكن تجاهل الدول التي يظهر اسمها داخل اسم دولة أخرى، مثل السودان.
2.20*: بيانات الدول، الخطوة 3
أضف إلى الواجهة التي تعرض بيانات دولة واحدة تقرير الطقس الخاص بعاصمة تلك الدولة. هناك عشرات مزوّدي بيانات الطقس. أحد الـAPIs المقترحة هو https://openweathermap.org. لاحظ أنه قد يستغرق الأمر بضع دقائق حتى يصبح مفتاح API المُولَّد صالحاً.

إذا استخدمت Open weather map، فهنا وصف لكيفية الحصول على أيقونات الطقس.
ملاحظة: في بعض المتصفحات (مثل Firefox) قد يرسل الـAPI المختار استجابة خطأ تشير إلى أن تشفير HTTPS غير مدعوم، رغم أن رابط الطلب يبدأ بـ_http://_. يمكن إصلاح هذه المشكلة بإكمال التمرين باستخدام Chrome.
ملاحظة: تحتاج إلى مفتاح api-key لاستخدام أي خدمة طقس تقريباً. لا تحفظ مفتاح api-key في نظام التحكم بالإصدارات! ولا تكتبه مباشرة في شيفرتك المصدرية. بدلاً من ذلك استخدم متغير بيئة لحفظ المفتاح في هذا التمرين. في التطبيقات الواقعية، يُعتبر إرسال هذه المفاتيح مباشرة من المتصفح غير آمن، لأن أي شخص يستطيع فتح وحدة تحكم المطوّر سيتمكن من اعتراض مفاتيحك! سنركز على تنفيذ واجهة خلفية منفصلة في الجزء التالي من الدورة.
بافتراض أن مفتاح api-key هو 54l41n3n4v41m34rv0، فعند بدء التطبيق هكذا:
export VITE_SOME_KEY=54l41n3n4v41m34rv0 && npm run dev // لنظام Linux/macOS Bash
($env:VITE_SOME_KEY="54l41n3n4v41m34rv0") -and (npm run dev) // لنظام Windows PowerShell
set "VITE_SOME_KEY=54l41n3n4v41m34rv0" && npm run dev // لنظام Windows cmd.exe
يمكنك الوصول إلى قيمة المفتاح من الكائن import.meta.env:
const api_key = import.meta.env.VITE_SOME_KEY
// المتغير api_key يحمل الآن القيمة المعيّنة عند بدء التشغيل
ملاحظة: لمنع تسريب متغيرات البيئة إلى العميل عن طريق الخطأ، لا يُعرَض لـVite إلا المتغيرات المسبوقة بـVITE_.
وتذكّر أيضاً أنه إذا أجريت تغييرات على متغيرات البيئة، فستحتاج إلى إعادة تشغيل خادم التطوير حتى تسري التغييرات.
كان هذا آخر تمرين في هذا الجزء من الدورة. حان وقت رفع شيفرتك إلى GitHub وتعليم جميع تمارينك المنجزة في نظام تسليم التمارين.