React و Next.js — من المكوّن الأول إلى صفحة هبوط عربية
لمن يعرف أساسيات JavaScript: تبني واجهات بمكوّنات وحالة وتأثيرات مدروسة في React 19، ثم تنتقل إلى Next.js بـ App Router ومكوّنات الخادم وServer Actions، وتنشر صفحة هبوط عربية باتجاه RTL.
كيف تدرس هذا المسار؟
ثبّت Node.js وافتح مشروع React تجريبي.
انسخ أمثلة المكوّنات وجرّبها في المتصفح.
نفّذ المهمة — كل درس يبني صفحتك.
أكمل الاختبار — التقدّم يُحفظ على جهازك.
مسار مكتوب لمن أنهى أساسيات HTML وJavaScript ويريد بناء واجهات حديثة بمكوّنات قابلة لإعادة الاستخدام.
1. لماذا React؟ — واجهات من قطع صغيرة
الهدف: تفهم المشكلة التي يحلها React، وفكرة أن الواجهة نتيجة للحالة، وتقرر متى يستحق مشروعك React ومتى لا.
هذا المسار موجّه لمن يعرف أساسيات JavaScript: الدوال والدوال السهمية، والمصفوفات مع map وfilter، والكائنات، والوحدات بـ import وexport. إذا كانت هذه المفاهيم غير مستقرة عندك فارجع إليها أولاً في مسار JavaScript، لأن معظم ما يبدو صعوبة في React هو في الحقيقة صعوبة في JavaScript نفسها. نبدأ هنا من السؤال الأهم: ما المشكلة التي جاء React ليحلها؟
تخيّل متجراً إلكترونياً صغيراً افتراضياً اسمه «بُنّ الصباح» يبيع القهوة. في رأس الصفحة شارة تعرض عدد المنتجات في السلة، وفي أسفلها ملخص يقول «لديك 3 منتجات». كتب المطوّر الأول الصفحة بتعديل DOM مباشرة، فصار كل زر مسؤولاً عن تذكّر كل مكان يعرض الرقم:
يعمل هذا الكود في البداية. ثم أضاف المطوّر زر حذف ونسي تحديث الملخص داخله، فصارت الشارة تعرض 2 والملخص يعرض 3. هذا النوع من الأخطاء لا يأتي من قلة المهارة؛ سببه أن الأسلوب الأمري (Imperative) يجعلك مسؤولاً عن كل خطوة تعديل، وكلما زادت الأماكن التي تعرض البيانات نفسها زادت فرص النسيان.
يعتمد React الأسلوب التصريحي (Declarative): تكتب مرة واحدة كيف تبدو الواجهة لأي قيمة من قيم البيانات، ثم تغيّر البيانات فقط، ويتولى React مطابقة الصفحة معها. هكذا تبدو الصفحة نفسها:
لا يوجد هنا أي سطر يقول «اذهب إلى الشارة وغيّر نصها». الزران يغيّران قيمة واحدة اسمها count، والشارة والملخص يقرآنها في كل مرة، فيستحيل أن يختلفا. هذه هي الفكرة المركزية في React: الواجهة نتيجة للحالة، أي أن ما تراه على الشاشة دالة في البيانات الحالية.
ماذا يحدث خلف الكواليس؟ عندما تتغير الحالة يستدعي React دالة المكوّن من جديد، وتسمى هذه المرحلة العرض (Render). ثم يقارن الناتج بالناتج السابق، ويطبّق على DOM الحقيقي التغييرات اللازمة فقط، وتسمى هذه المرحلة الإثبات (Commit). يُشاع أن هذه المقارنة تجعل React أسرع من التعامل مع DOM مباشرة، وهذا غير دقيق؛ فالتعديل اليدوي المدروس قد يكون أسرع. ميزة React الحقيقية أن التحديث يبقى صحيحاً ومنظماً حين يكبر التطبيق ويتعدد العاملون عليه.
المكوّنات (Components) هي الوحدة التي تبني بها كل ذلك: دالة JavaScript ترجع وصفاً لجزء من الواجهة. الشارة مكوّن، وبطاقة المنتج مكوّن، والصفحة كلها مكوّن يركّب البقية. وReact نفسه مكتبة للواجهة فقط، فلا يقرر لك كيف تنظم الصفحات أو تجلب البيانات من الخادم. لهذا يوصي توثيق react.dev ببدء المشاريع الجديدة بإطار عمل (Framework) مثل Next.js الذي نصل إليه في الدرس الثامن، أو بأداة بناء مثل Vite حين تريد تطبيقاً يعمل في المتصفح وتختار بقية الأدوات بنفسك. أما أداة Create React App القديمة فقد أعلن فريق React إيقاف التوصية بها للمشاريع الجديدة.
هل يستحق مشروعك React؟ اطرح ثلاثة أسئلة قبل البدء:
• هل توجد بيانات تتغير أثناء بقاء الزائر في الصفحة وتظهر في أكثر من مكان؟
• هل ستتكرر قطع متشابهة كثيراً، مثل بطاقات المنتجات أو صفوف الجداول؟
• هل سيكبر المشروع لاحقاً ويعمل عليه أكثر من شخص؟
إن كانت الإجابات كلها بالنفي، كصفحة تعريف بمطعم أو مدونة بمحتوى ثابت، فـ HTML وCSS مع قليل من JavaScript أبسط وأخف وأسرع تحميلاً. وإن كانت إجابة واحدة بالإيجاب، فـ React خيار منطقي يستحق تعقيده الإضافي مقابل تنظيم ستحتاجه حتماً.
React يبني الواجهة من مكوّنات، وكل مكوّن دالة ترجع وصفاً لجزء من الصفحة.
الأسلوب التصريحي: تصف الواجهة لكل حالة، وتغيّر البيانات فقط، وReact يطابق الصفحة معها.
عند تغيّر الحالة يعيد React العرض ثم يطبّق على DOM الفروق اللازمة وحدها.
لمشروع جديد يوصي react.dev بإطار مثل Next.js أو بأداة بناء مثل Vite.
مخطط
بيانات (state)
↓
مكوّنات React
↓
عرض←مقارنة
↓
DOM: الفروق فقط
مختبر: أين تتكرر البيانات؟
افتح صفحة متجر تستخدمه، واكتب ثلاث قيم تتغير أثناء التصفح (عدد السلة، نتائج البحث، المجموع)، ولكل قيمة كل الأماكن التي تظهر فيها. هذه الأماكن هي ما سيقرأ الحالة نفسها في React.
مهمة عملية
أعد كتابة مثال السلة بتعديل DOM مباشرة وأضف زر حذف، ثم اكتب في ثلاث جمل أين يمكن أن يختلف الرقمان، ولماذا لا يحدث ذلك في نسخة React.
اختبر فهمك (1/3)
ما الذي يجعل الشارة والملخص في نسخة React من مثال السلة لا يختلفان أبداً؟
اختبر فهمك (2/3)
أي وصف لآلية تحديث React هو الأدق؟
اختبر فهمك (3/3)
مطعم يريد صفحة تعريف ثابتة فيها القائمة والعنوان وساعات العمل فقط. ما القرار المعقول؟
الهدف: تنشئ مشروع React بـ Vite، وتكتب مكوّنات نقية، وتنظم استيرادها وتصديرها دون الأخطاء الشائعة.
لإنشاء تطبيق React يعمل في المتصفح دون إطار عمل، الأداة الشائعة اليوم هي Vite، وهي من الخيارات التي يذكرها توثيق react.dev لبناء تطبيق من الصفر. تحتاج قبلها Node.js بإصدار LTS حديث، ثم تنفّذ في الطرفية:
npm create vite@latest coffee-shop -- --template react
cd coffee-shop
npm install
npm run dev
الشرطتان -- تمرران ما بعدهما إلى أداة Vite نفسها بدلاً من npm، فيصل خيار القالب سليماً. قد يطرح المثبّت أسئلة إضافية تختلف بين الإصدارات، والإعدادات الافتراضية مناسبة للبداية. بعد التشغيل تطبع الطرفية رابط خادم التطوير، وهو افتراضياً http://localhost:5173، وكل حفظ لملف يظهر أثره فوراً في المتصفح دون إعادة تحميل يدوية.
المكوّن دالة JavaScript يبدأ اسمها بحرف كبير وترجع JSX. أنشئ ملفين، الأول للبطاقة والثاني للتطبيق الذي يستخدمها:
استدعيت ProductCard مرتين فظهرت بطاقتان من تعريف واحد. البطاقتان متطابقتان الآن، وفي الدرس الرابع ستمرر لكل واحدة بيانات مختلفة عبر props.
التصدير والاستيراد. للملف تصدير افتراضي (default) واحد على الأكثر، وتستورده بلا أقواس معقوفة وبأي اسم تختاره. ويمكن أن يحتوي عدداً من التصديرات المسمّاة (named)، وتستوردها بين أقواس معقوفة وبالاسم نفسه حرفياً، مثل import { Badge } from "./components/Badge". كثير من الفرق تفضّل التصدير المسمّى لأن المحرر يكتشف الأسماء الخاطئة فوراً؛ المهم أن تختار أسلوباً واحداً وتلتزم به في المشروع كله.
المكوّن يجب أن يكون نقياً (Pure). معنى ذلك أنه لا يغيّر أثناء العرض أي شيء كان موجوداً قبل استدعائه، وأنه يرجع الناتج نفسه للمدخلات نفسها. المثال التالي يخالف القاعدة لأنه يزيد متغيراً خارجياً في كل عرض:
let guest = 0;
function Cup() {
guest = guest + 1;
return <p>الكوب رقم {guest}</p>;
}
في وضع التطوير يغلّف قالب Vite التطبيق بـ StrictMode، الذي يعرض كل مكوّن مرة إضافية عمداً ليكشف هذا النوع من الأخطاء، فترى أرقاماً تقفز بشكل غير متوقع. الحل أن تأتي البيانات من props أو من الحالة، وأن يجري أي تعديل خارجي داخل معالج حدث يُنفّذ بعد نقرة المستخدم.
سجل أعطال من مشروع طالبة افتراضية اسمها سلمى، مع سبب كل عطل وإصلاحه:
• الصفحة بيضاء وفي Console رسالة تفيد بأن الملف لا يوفّر تصديراً باسم ProductCard: كانت قد كتبت export default ثم استوردت بأقواس معقوفة. الإصلاح توحيد طريقة التصدير والاستيراد.
• رسالة Vite بعبارة Failed to resolve import: المسار بعد from خاطئ. تذكّر أن ./ تشير إلى المجلد الذي يقع فيه الملف الحالي، فالمسار من App.jsx إلى المكوّنات هو ./components/ProductCard.
• المكوّن لا يظهر وفي Console تحذير عن وسم غير معروف: سمّته card بحرف صغير، فعامله React كوسم HTML عادي. الإصلاح تسميته Card.
• حقل نص داخل بطاقة يفقد ما كُتب فيه مع كل تحديث: كانت قد عرّفت مكوّناً داخل جسم مكوّن آخر، فينشأ مكوّن جديد في كل عرض وتضيع حالته. الإصلاح تعريف كل مكوّن في المستوى الأعلى من الملف.
• خطأ يذكر أن صيغة JSX غير مفعّلة: كتبت المكوّن في ملف امتداده js. قالب Vite يتوقع JSX في ملفات امتدادها jsx، فالإصلاح إعادة تسمية الملف وتعديل مسار الاستيراد إن لزم.
قاعدة التقسيم العملية: إذا احتجت أن تصف ما يفعله مكوّن بحرف العطف «و» أكثر من مرة، كأن تقول «يعرض المنتج ويحسب السلة ويرسل الطلب»، فقسّمه إلى مكوّنات أصغر. ووضع كل مكوّن في ملف يحمل اسمه داخل src/components/ يجعل العثور عليه وتعديله أسهل لك ولزملائك.
مشروع React بلا إطار يُنشأ عادة بـ Vite ويعمل على المنفذ 5173 افتراضياً.
اسم المكوّن يبدأ بحرف كبير، وإلا عامله React كوسم HTML.
طريقة الاستيراد تطابق طريقة التصدير: الافتراضي بلا أقواس، والمسمّى بأقواس وبالاسم نفسه.
المكوّن النقي لا يعدّل متغيرات خارجية أثناء العرض، وStrictMode يكشف المخالفة.
لا تعرّف مكوّناً داخل جسم مكوّن آخر؛ عرّف كل مكوّن في المستوى الأعلى من الملف.
مخطط
App.jsx
|
+--+--+--+
v v v v
Header Card Badge Footer
\ | /
src/components/
مختبر: ثلاثة مكوّنات وتصديران
أنشئ Header وFooter بتصدير افتراضي، وBadge بتصدير مسمّى، واستورد الثلاثة في App. ثم اكسر الاستيراد عمداً مرة واحدة واقرأ رسالة الخطأ كاملة قبل إصلاحها.
مهمة عملية
أنشئ مشروع Vite جديداً، وأضف مكوّنين على الأقل في src/components يظهران في الصفحة، ثم شغّل npm run build وتأكد أنه ينتهي دون أخطاء.
اختبر فهمك (1/3)
مكوّن عرّفته داخل جسم مكوّن App، وفيه حقل نص يفقد محتواه مع كل تحديث. ما السبب؟
اختبر فهمك (2/3)
في Badge.jsx كتبت export function Badge(). أي سطر استيراد صحيح؟
اختبر فهمك (3/3)
مكوّن يزيد متغيراً خارجياً بالسطر guest = guest + 1 أثناء العرض. ما المشكلة؟
الهدف: تحوّل HTML إلى JSX صحيح، وتستخدم التعبيرات والشروط داخل الأقواس المعقوفة دون الوقوع في فخاخها.
JSX امتداد لصيغة JavaScript يسمح بكتابة وسوم تشبه HTML داخل الكود. المتصفح لا يفهم JSX مباشرة، فأداة البناء تحوّلها قبل التشغيل إلى استدعاءات دوال عادية تُنشئ وصفاً للعناصر. لهذا تخضع JSX لقواعد JavaScript، وهذا وحده يفسّر كل الفروق التي ستلاحظها عن HTML.
أسرع طريقة لتعلّم هذه الفروق أن تحوّل قطعة HTML حقيقية بيدك. هذه بطاقة اشتراك في نشرة بريدية مأخوذة من صفحة ثابتة:
<div class="newsletter">
<!-- نموذج الاشتراك -->
<label for="mail">بريدك</label>
<input id="mail" type="email">
<img src="/cup.png" alt="">
<p style="color: gray; font-size: 14px">رسالة واحدة كل أسبوع</p>
</div>
وهذه هي البطاقة نفسها بعد التحويل إلى JSX:
<div className="newsletter">
{/* نموذج الاشتراك */}
<label htmlFor="mail">بريدك</label>
<input id="mail" type="email" />
<img src="/cup.png" alt="" />
<p style={{ color: "gray", fontSize: 14 }}>رسالة واحدة كل أسبوع</p>
</div>
حدثت خمسة تغييرات، ولكل منها سبب. صارت class هي className وصارت for هي htmlFor لأن الكلمتين محجوزتان في JavaScript. والوسوم التي لا تُغلق في HTML مثل input وimg يجب أن تُغلق هنا بشرطة مائلة. وصار التعليق داخل أقواس معقوفة لأن ما بينها كود JavaScript. وصارت style تأخذ كائناً بخصائص مكتوبة بصيغة camelCase، والقوسان المزدوجان هما قوس JSX وفي داخله قوس الكائن. ولاحظ أن الرقم 14 كُتب بلا وحدة، فيضيف React وحدة px تلقائياً للخصائص الرقمية من هذا النوع.
الأقواس المعقوفة تقبل تعبيرات فقط. التعبير (Expression) هو ما ينتج قيمة: متغير، أو عملية حسابية، أو استدعاء دالة، أو شرط ثلاثي. أما الجمل (Statements) مثل if وfor فلا تُكتب داخلها. للعرض الشرطي ثلاثة أنماط، ولكل نمط موضعه المناسب:
الإرجاع المبكر بـ if قبل JSX يناسب حالة تلغي العرض كله، كبيانات لم تصل بعد. والشرط الثلاثي يناسب الاختيار بين بديلين. والعامل && يناسب عنصراً يظهر أو يختفي. وإرجاع null من مكوّن يعني ببساطة «لا تعرض شيئاً»، وهو مسموح تماماً. وحين يطول الشرط ويزدحم JSX، احفظ العنصر في متغير قبل return، مثل let note = null; if (qty < 5) note = <p>كمية محدودة</p>;، ثم ضعه بين الأقواس بكتابة {note}.
فخ الصفر من أشهر أخطاء JSX. لو كتبت {qty && <p>متوفر</p>} وكانت القيمة 0، فسيظهر الرقم 0 وحيداً على الصفحة، لأن && يعيد قيمته اليسرى حين تكون «خاطئة»، وReact يعرض الأرقام. القيم false وnull وundefined لا يُعرض منها شيء، أما 0 فيُعرض. الحل أن تجعل الطرف الأيسر قيمة منطقية صريحة دائماً، مثل qty > 0.
جذر واحد لكل إرجاع. الدالة في JavaScript ترجع قيمة واحدة، لذلك لا يمكن أن يرجع المكوّن وسمين متجاورين. إن فعلت ظهرت رسالة تطلب تغليف عناصر JSX المتجاورة بوسم واحد. استخدم Fragment بالصيغة المختصرة <>...</> حين لا تريد إضافة عنصر زائد إلى الصفحة، أو الصيغة الكاملة <Fragment key={id}> حين تحتاج مفتاحاً داخل قائمة.
الحماية من حقن السكربتات مدمجة: أي نص تضعه بين الأقواس يُهرَّب (Escaped) قبل عرضه، فإن احتوى تعليق زائر على وسم script فسيظهر نصاً عادياً ولن يُنفّذ. تتجاوز هذه الحماية فقط حين تستخدم الخاصية dangerouslySetInnerHTML، واسمها تحذير مقصود، فلا تمرر لها محتوى كتبه المستخدمون قبل تنقيته بمكتبة موثوقة.
رسالة أخيرة ستقابلها حتماً: Objects are not valid as a React child. تظهر حين تضع كائناً كاملاً بين الأقواس، مثل {user} بينما user كائن فيه الاسم والبريد. React يعرف كيف يعرض النصوص والأرقام والعناصر ومصفوفاتها، ولا يعرف كيف يحوّل كائناً عادياً إلى شيء مرئي. الإصلاح أن تختار الحقل المطلوب مثل {user.name}، وإن أردت رؤية الكائن كله مؤقتاً أثناء التصحيح فاكتب {JSON.stringify(user)} ثم احذفه قبل التسليم.
JSX تتحول إلى استدعاءات JavaScript، لذلك className بدل class وhtmlFor بدل for.
الأقواس المعقوفة تقبل تعبيرات فقط؛ للشرط استخدم الثلاثي أو && أو إرجاعاً مبكراً قبل JSX.
الرقم 0 يُعرض على الصفحة، فاجعل شرط && قيمة منطقية صريحة.
Fragment يغلّف عناصر متجاورة دون وسم زائد، والنص داخل {} يُهرَّب تلقائياً.
مخطط
HTML
↓
JSX←أداة البناء←دوال JavaScript
مختبر: حوّل HTML إلى JSX
انسخ نموذج تسجيل من صفحة HTML تعليمية، وحوّله إلى JSX يدوياً. عدّ التغييرات التي أجريتها، ثم ضعه في مكوّن وتأكد أن Console خالٍ من التحذيرات.
مهمة عملية
اكتب مكوّن ProductBadge يعرض «متوفر» أو «نفد المخزون» حسب المتغير qty، ويعرض «آخر القطع» فقط حين تكون الكمية بين 1 و4. اختبره بالقيم 0 و3 و10 وسجّل ما يظهر في كل حالة.
اختبر فهمك (1/3)
أي سطر JSX صحيح لعنصر label مرتبط بحقل معرّفه mail؟
اختبر فهمك (2/3)
كتبت {qty && <p>متوفر</p>} وقيمة qty تساوي 0. ماذا يظهر في الصفحة؟
اختبر فهمك (3/3)
أي مما يلي لا يمكن كتابته مباشرة داخل الأقواس المعقوفة في JSX؟
الهدف: تقرر بثقة أين تعيش كل قطعة بيانات: props من الأب، أو حالة داخل المكوّن، أو قيمة تُحسب أثناء العرض.
كل بيانات واجهة React تنتمي إلى أحد ثلاثة أنواع، ومعظم الأخطاء التي يقع فيها المتعلّمون سببها وضع البيانات في النوع الخطأ. النوع الأول الخصائص (Props): قيم يمررها المكوّن الأب إلى ابنه كما تمرر وسائط إلى دالة. النوع الثاني الحالة (State): ذاكرة يملكها المكوّن وتتغير مع تفاعل المستخدم. والنوع الثالث، الذي يغفله كثيرون: قيم مشتقة (Derived) تُحسب من النوعين السابقين أثناء العرض ولا تُخزَّن أبداً.
تمرير props يشبه كتابة خصائص HTML، مع فرق أن القيمة يمكن أن تكون أي شيء في JavaScript:
النصوص تُمرر بعلامات تنصيص، والأرقام والمصفوفات والدوال بين أقواس معقوفة، وlevel = "مبتدئ" قيمة افتراضية تُستخدم حين لا يمررها الأب. وما تضعه بين وسمي فتح المكوّن وإغلاقه يصل إليه في خاصية اسمها children، وهذا ما يسمح ببناء أغلفة عامة مثل صندوق تنبيه أو نافذة منبثقة تستقبل أي محتوى.
الخصائص للقراءة فقط من جهة الابن. لو كتب الابن props.title = "جديد" فلن يتغير شيء على الشاشة، لأن React لا يعيد العرض بسبب تعديل كهذا، وتكون قد كسرت قاعدة النقاء التي رأيتها في الدرس السابق. إذا احتاج الابن أن يغيّر قيمة يملكها الأب، فالأب يمرر له دالة، والابن يستدعيها عند الحاجة. الاصطلاح الشائع أن يبدأ اسم هذه الخاصية بـ on، مثل <SearchBox query={query} onQueryChange={setQuery} />، فيفهم قارئ الكود فوراً أنها دالة تُستدعى عند حدث ما.
ثلاثة أسئلة لكل قطعة بيانات، مستمدة من دليل Thinking in React في التوثيق الرسمي:
1) هل تأتي من الأب؟ إذن هي props.
2) هل تبقى ثابتة طوال الوقت؟ إذن لا تحتاج حالة، ويكفي ثابت عادي أو prop.
3) هل يمكن حسابها من props أو حالة موجودة؟ إذن احسبها أثناء العرض ولا تخزّنها.
ما يتبقى بعد هذه الأسئلة الثلاثة هو الحالة فعلاً.
لنطبّق ذلك على مركز تدريب افتراضي اسمه «منارة» يعرض دليل دورات. قائمة الدورات تأتي من الخادم وتصل إلى صفحة الدليل في prop اسمها courses. نص البحث يتغير مع الكتابة، فهو حالة. أما «الدورات المطابقة للبحث» و«عدد النتائج» فيمكن حسابهما من القائمة ونص البحث، فهما قيمتان مشتقتان. هذه نسخة مبتدئ نموذجية تخزّن النتائج في حالة ثانية:
تعمل هذه النسخة إلى أن تتغير قائمة الدورات القادمة من الأب، كأن تُضاف دورة جديدة، فتبقى results قديمة لأن أحداً لم يحدّثها. النسخة الأصح تحذف الحالة الثانية تماماً:
السطر الثاني يُنفّذ في كل عرض، فيبقى متزامناً تلقائياً مع القائمة ونص البحث، ويصبح عدد النتائج مجرد results.length. لا تقلق من تكلفة التصفية في قوائم من عشرات أو مئات العناصر؛ وحين تقيس بطئاً حقيقياً مع قوائم ضخمة توجد أدوات للتخزين المؤقت مثل useMemo.
رفع الحالة (Lifting State Up). إذا وضعت حقل البحث في مكوّن SearchBox وقائمة النتائج في مكوّن ResultsList، فكلاهما يحتاج نص البحث. لا تعطِ كلاً منهما حالة مستقلة، لأن النسختين ستختلفان عاجلاً أو آجلاً. ضع الحالة في أقرب أب مشترك، ومرر query إلى القائمة، ومرر query وsetQuery إلى صندوق البحث. هكذا يبقى لكل معلومة مصدر واحد للحقيقة (Single Source of Truth)، وأي خلل تبحث عنه في مكان واحد.
Props تأتي من الأب وهي للقراءة فقط، والابن يغيّر بيانات الأب باستدعاء دالة يمررها له.
الحالة ذاكرة المكوّن لما يتغير مع التفاعل، مثل نص حقل أو قائمة مفتوحة.
كل قيمة يمكن حسابها من props أو حالة تُحسب أثناء العرض ولا تُخزَّن.
حين يحتاج مكوّنان شقيقان البيانات نفسها، ارفع الحالة إلى أقرب أب مشترك.
مخطط
Page (state: query)
| |
query+setQuery query
v v
SearchBox ResultsList
|
results = courses.filter(...)
مختبر: صنّف البيانات
لصفحة دليل دورات فيها بحث وفلتر مستوى وعدّاد نتائج، اكتب جدولاً من ثلاثة أعمدة: props، حالة، مشتق. ضع فيه كل قطعة بيانات، ثم طبّق الأسئلة الثلاثة من الدرس للتحقق.
مهمة عملية
ابنِ صفحة فيها مصفوفة من خمس دورات، وحقل بحث في مكوّن SearchBox، وقائمة في مكوّن ResultsList. ضع الحالة في الأب، واحسب النتائج وعددها أثناء العرض دون أي حالة إضافية.
اختبر فهمك (1/3)
في دليل الدورات، أي قيمة يجب ألا تُخزَّن في حالة؟
اختبر فهمك (2/3)
حقل بحث في SearchBox وقائمة في ResultsList يحتاجان نص البحث نفسه. ما الحل؟
اختبر فهمك (3/3)
خزّنت نتائج البحث في حالة ثانية results، ثم تغيّرت قائمة الدورات القادمة من الأب. ماذا يحدث؟
الهدف: تستخدم useState بثقة: تفهم أن الحالة لقطة ثابتة في كل عرض، وتحدّث الأرقام والكائنات والمصفوفات بالطريقة الصحيحة.
يرجع useState مصفوفة من عنصرين: القيمة الحالية، ودالة تطلب تغييرها. القيمة التي تمررها له هي القيمة الابتدائية، ولا تُستخدم إلا في أول عرض للمكوّن؛ بعد ذلك يتذكر React آخر قيمة بين العروض. والاصطلاح أن تسمي الزوج [something, setSomething]. هذا مكوّن لتسجيل الحضور في نادٍ رياضي صغير افتراضي، فيه ثلاثة أنواع من الحالة:
أهم فكرة في هذا الدرس أن الحالة لقطة (Snapshot). في كل عرض تكون count ثابتاً عادياً بقيمة محددة، واستدعاء setCount لا يغيّر هذا الثابت؛ هو يطلب من React عرضاً جديداً تكون فيه القيمة مختلفة. الأعطال الأربعة التالية وقعت في هذا المشروع أثناء تطويره، وكلها تعود إلى هذه الفكرة بشكل أو بآخر.
العطل الأول: زر «+2» يضيف واحداً فقط. كتب المطوّر:
function addTwo() {
setCount(count + 1);
setCount(count + 1);
}
لو كانت القيمة 5، فكلا السطرين يقرأ اللقطة نفسها ويطلب «اجعلها 6». يجمع React التحديثات ويطبقها بعد انتهاء المعالج، فتكون النتيجة 6. الإصلاح دالة تحديث (Updater Function) تستلم أحدث قيمة في الطابور: setCount(c => c + 1) مرتين تعطي 7. القاعدة العملية: كلما اعتمدت القيمة الجديدة على السابقة، مرّر دالة.
العطل الثاني: سجل الحضور لا يتحدث على الشاشة. كان الكود log.push(entry) ثم setLog(log). الدالة push تعدّل المصفوفة نفسها، فيقارن React المرجع الجديد بالقديم فيجدهما الشيء ذاته ويتجاهل التحديث. الإصلاح إنشاء مصفوفة جديدة في كل عملية:
setLog([...log, entry]); // إضافة
setLog(log.filter(e => e.id !== id)); // حذف
setLog(log.map(e => (e.id === id ? { ...e, late: true } : e))); // تعديل عنصر
العطل الثالث: تعديل نوع الاشتراك يمسح الاسم. كتب المطوّر setMember({ plan: "سنوي" }) فاختفى حقل الاسم من الكائن. دالة التحديث في useState تستبدل القيمة كاملة ولا تدمجها مع القديمة. الإصلاح نسخ الكائن ثم تغيير الحقل المطلوب: setMember({ ...member, plan: "سنوي" }).
العطل الرابع: رسالة Too many re-renders تملأ الشاشة. كان الزر مكتوباً onClick={setCount(count + 1)}. الأقواس بعد اسم الدالة تستدعيها أثناء العرض نفسه، فيطلب التحديث عرضاً جديداً، فيُستدعى مرة أخرى، حتى يوقف React الحلقة. الصحيح تمرير دالة تُنفّذ عند النقر: onClick={() => setCount(count + 1)}.
الحقول المتحكَّم بها (Controlled Inputs) تطبيق مباشر لكل ما سبق: قيمة الحقل تأتي من الحالة، وكل حرف يُكتب يحدّث الحالة بنسخة جديدة من الكائن:
إذا وضعت value دون onChange صار الحقل للقراءة فقط وظهر تحذير في Console. وإذا بدأت القيمة undefined ثم صارت نصاً، يحذرك React من تحوّل الحقل من غير متحكَّم به إلى متحكَّم به؛ لذلك ابدأ دائماً بنص فارغ كما في الحالة الابتدائية أعلاه.
وإذا كانت القيمة الابتدائية تحتاج حساباً، كقراءة سجل محفوظ من قبل، فمرّر دالة بدلاً من القيمة: useState(() => JSON.parse(localStorage.getItem("log") ?? "[]")). هكذا تُنفّذ القراءة في أول عرض فقط. لو كتبت useState(JSON.parse(...)) مباشرة لتكرر التحليل مع كل عرض ثم أُهملت نتيجته، وهو هدر لا تراه لكنه يتراكم في القوائم الكبيرة.
قواعد الخطافات (Rules of Hooks): استدعِ useState وكل دالة يبدأ اسمها بـ use في المستوى الأعلى من المكوّن فقط، وتجنّب وضعها داخل شرط أو حلقة أو دالة متداخلة. يربط React كل حالة بترتيب استدعائها في المكوّن، فإن تغيّر هذا الترتيب بين عرض وآخر اختلطت القيم وظهرت أخطاء يصعب تتبعها. وقالب React في Vite يتضمن عادةً قواعد ESLint الخاصة بالخطافات، فيضع المحرر خطاً تحت أي مخالفة من هذا النوع قبل أن تشغّل الكود أصلاً.
الحالة لقطة ثابتة في كل عرض، وsetState يطلب عرضاً جديداً دون أن يغيّر المتغير الحالي.
إذا اعتمدت القيمة الجديدة على السابقة فاستخدم دالة تحديث مثل c => c + 1.
الحقل المتحكَّم به يحتاج value وonChange معاً، وقيمة ابتدائية نصية بدل undefined.
مخطط
نقرة←setCount(c←c + 1)
↓
طابور التحديثات
↓
عرض جديد بلقطة جديدة
مختبر: أصلح الأعطال الأربعة
انسخ الأعطال الأربعة من الدرس إلى مكوّن واحد كما هي، وشاهد كل عطل في المتصفح قبل إصلاحه. اكتب تحت كل إصلاح تعليقاً من سطر واحد يشرح السبب.
مهمة عملية
ابنِ مكوّن حضور فيه حقل اسم متحكَّم به، وزر يضيف الاسم إلى سجل مع وقت التسجيل، وزر حذف لكل سطر، وعدّاد يعرض عدد الحاضرين محسوباً من طول السجل. تأكد أن Console خالٍ من التحذيرات.
اختبر فهمك (1/3)
قيمة count تساوي 5، ونفّذت داخل معالج واحد setCount(count + 1) مرتين. ما القيمة في العرض التالي؟
اختبر فهمك (2/3)
كتبت setMember({ plan: "سنوي" }) فاختفى الاسم من الكائن. ما الإصلاح؟
اختبر فهمك (3/3)
زر مكتوب onClick={setCount(count + 1)} يسبب رسالة Too many re-renders. لماذا؟
الهدف: تعرض قوائم بمفاتيح صحيحة، وتربط الأحداث دون أخطاء، وتختار بين onSubmit وخاصية action في نماذج React 19.
لا توجد حلقة for داخل JSX، والأداة الطبيعية لعرض قائمة هي map: تحوّل مصفوفة بيانات إلى مصفوفة عناصر. لنفترض مطبخاً منزلياً افتراضياً يستقبل طلبات توصيل ويعرضها على شاشة صغيرة بجانب الموقد:
المفتاح (key) هو هوية العنصر. عندما تتغير القائمة يحتاج React أن يعرف أي عنصر قديم يقابل أي عنصر جديد: هل حُذف الثاني؟ هل أضيف عنصر في البداية؟ المفتاح يجيب عن ذلك. له شرطان: أن يكون فريداً بين إخوته في القائمة نفسها، وأن يبقى ثابتاً للعنصر نفسه بين عرض وآخر. معرّف قاعدة البيانات هو الخيار المثالي. وإن نسيت المفتاح فسيطبع React تحذيراً في Console يطلب مفتاحاً فريداً لكل ابن في القائمة.
تجربة توضح لماذا لا يصلح رقم الترتيب (index) مفتاحاً في قائمة تتغير: أضف بجانب كل طلب حقل ملاحظة غير متحكَّم به، واكتب «بدون بصل» بجانب الطلب الأول، ثم احذف الطلب الأول. مع key={index} يصبح الطلب الثاني صاحب المفتاح 0، فيظن React أن العنصر 0 باقٍ وتغيّر نصه فقط، وتبقى عبارة «بدون بصل» ظاهرة بجانب الطلب الخطأ. مع key={order.id} يُحذف العنصر الصحيح بملاحظته. رقم الترتيب مقبول فقط لقائمة ثابتة لا تُحذف عناصرها ولا يُعاد ترتيبها. ولا تولّد المفتاح أثناء العرض بـ Math.random()، لأنه سيتغير مع كل عرض فيعيد React بناء العناصر كلها؛ ولِّد المعرّف مرة واحدة عند إنشاء العنصر.
الأحداث تحتاج دالة يستدعيها React لاحقاً عند وقوع الحدث. لاحظ الفرق بين الزرين:
السطر الأول يمرر الدالة نفسها. والثاني يغلّف الاستدعاء بدالة سهمية لأنه يحتاج تمرير معرّف الطلب. أما onClick={handleRefresh()} فيستدعي الدالة فوراً أثناء العرض. وإذا وضعت زراً داخل بطاقة قابلة للنقر وأردت ألا يصل حدث الزر إلى البطاقة، فاستدعِ e.stopPropagation() في معالج الزر.
ملاحظة وصول (Accessibility): وضع onClick على عنصر li أو div يجعله قابلاً للنقر بالفأرة فقط، فلا يصل إليه مستخدم لوحة المفاتيح بمفتاح Tab ولا تعلن عنه قارئات الشاشة كعنصر تفاعلي. إذا كان العنصر يؤدي فعلاً فاجعله button، وإذا كان ينقل إلى صفحة أخرى فاجعله رابطاً. والاصطلاح في التسمية أن يبدأ اسم المعالج بـ handle، مثل handleDelete، ويبدأ اسم الخاصية التي تستقبله بـ on.
للنماذج طريقتان. الطريقة الكلاسيكية معالج onSubmit مع حقل متحكَّم به:
إرسال النموذج في المتصفح يعيد تحميل الصفحة افتراضياً، وe.preventDefault() يلغي ذلك. ووضع المعالج على form بدلاً من onClick على الزر يجعل مفتاح Enter يرسل النموذج أيضاً، ويحافظ على السلوك الذي تتوقعه قارئات الشاشة من النماذج.
منذ React 19 يمكنك أيضاً تمرير دالة إلى خاصية action في النموذج. تستقبل الدالة كائن FormData يجمع الحقول حسب خاصية name، ولا تحتاج preventDefault، ويعيد React تعيين الحقول غير المتحكَّم بها بعد نجاح الدالة:
متى تختار أيهما؟ الحقل المتحكَّم به مناسب حين تحتاج القيمة أثناء الكتابة، كتحقق فوري أو اقتراحات بحث. وخاصية action مناسبة حين تهمك القيم عند الإرسال فقط، وهي الطريقة نفسها التي سترسل بها النماذج إلى الخادم عبر Server Actions في الدرس الأخير. وفي الحالتين، احذف العناصر بـ filter لتنشئ مصفوفة جديدة بدل تعديل القديمة، وأعطِ كل حقل عنصر label مرتبطاً به حتى يعرف مستخدم قارئ الشاشة ما المطلوب كتابته.
map يحوّل مصفوفة البيانات إلى عناصر، ولكل عنصر key فريد بين إخوته وثابت بين العروض.
رقم الترتيب مفتاح خطر في قائمة تُحذف عناصرها أو يُعاد ترتيبها.
onClick={fn} يمرر الدالة، وonClick={fn()} يستدعيها أثناء العرض.
onSubmit مع preventDefault للحقول المتحكَّم بها، وخاصية action مع FormData في React 19.
مخطط
orders
↓
.map()
↓
li · li · li
key=id (ثابت وفريد)
مختبر: تجربة المفتاح
نفّذ تجربة «بدون بصل» من الدرس مرتين: مرة بـ key={index} ومرة بـ key={order.id}. التقط صورة للشاشة بعد الحذف في الحالتين وقارن بينهما.
مهمة عملية
ابنِ لوحة طلبات تضيف طلباً بنموذج يستخدم خاصية action، وتعرض القائمة بـ map ومفتاح ثابت، وتحذف الطلب بزر يستخدم filter. تأكد أن Console لا يعرض أي تحذير عن المفاتيح.
اختبر فهمك (1/3)
أي قيمة هي الأنسب لخاصية key في قائمة طلبات تُحذف منها عناصر؟
اختبر فهمك (2/3)
ما الذي يميّز تمرير دالة إلى خاصية action في نموذج React 19؟
اختبر فهمك (3/3)
حذفت الطلب الأول من قائمة مفتاحها index فبقيت ملاحظته المكتوبة بجانب طلب آخر. لماذا؟
الهدف: تستخدم useEffect للمزامنة مع أنظمة خارجية فقط، وتجلب البيانات مع حالات التحميل والخطأ ومنع سباق الردود، وتتعرف على الحالات التي لا تحتاج فيها Effect أصلاً.
التأثير (Effect) في React كود يعمل بعد ظهور المكوّن على الشاشة، ووظيفته مزامنة المكوّن مع نظام خارج React: طلب شبكة، أو اتصال WebSocket، أو مؤقت، أو مكتبة خرائط تتعامل مع DOM مباشرة. هذا التعريف هو مفتاح الدرس كله، لأن أكثر أخطاء useEffect شيوعاً هي استخدامه حيث لا يوجد نظام خارجي أصلاً. وقد خصص توثيق React لذلك صفحة كاملة بعنوان You Might Not Need an Effect.
القاعدة السريعة: قبل أن تكتب useEffect اسأل: هل يجب أن يحدث هذا الكود لأن المكوّن ظهر على الشاشة، أم لأن المستخدم فعل شيئاً؟ إن كان سببه فعل المستخدم فمكانه معالج الحدث. وإن كان مجرد حساب من props أو حالة فمكانه جسم المكوّن. يبقى useEffect لما يجب أن يحدث بسبب الظهور نفسه.
ثلاث حالات شائعة لا تحتاج Effect:
• حساب قيمة مشتقة: تخزين مجموع السلة في حالة وتحديثه بـ Effect كلما تغيّرت العناصر يضيف عرضاً زائداً ومصدراً ثانياً للحقيقة. اكتب const total = items.reduce((s, i) => s + i.price, 0); في جسم المكوّن مباشرة.
• الاستجابة لنقرة: إرسال طلب شراء داخل Effect يراقب حالة اسمها submitted يجعل التدفق ملتوياً، وقد يُرسل الطلب مرة ثانية إذا ظهر المكوّن من جديد. أرسل الطلب داخل معالج النقر نفسه.
• تصفير الحالة عند تغيّر المستخدم: بدلاً من Effect يمسح حقل التعليق كلما تغيّر userId، مرّر key={userId} إلى المكوّن، فيعيد React إنشاءه بحالة جديدة.
أما جلب البيانات حين يظهر المكوّن فهو مزامنة حقيقية مع الخادم، وهذا شكله الكامل في تطبيق React يعمل في المتصفح:
import { useEffect, useState } from "react";
export default function Profile({ userId }) {
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
let ignore = false;
setUser(null);
setError(null);
fetch("https://jsonplaceholder.typicode.com/users/" + userId)
.then(res => {
if (!res.ok) throw new Error("HTTP " + res.status);
return res.json();
})
.then(data => { if (!ignore) setUser(data); })
.catch(err => { if (!ignore) setError(err.message); });
return () => { ignore = true; };
}, [userId]);
if (error) return <p>تعذّر التحميل: {error}</p>;
if (!user) return <p>جارٍ التحميل...</p>;
return <h2>{user.name}</h2>;
}
تتبّع ما يحدث في هذا الكود خطوة بخطوة:
1) مصفوفة الاعتماديات [userId] تعني: شغّل التأثير بعد أول ظهور، ثم كلما تغيّر userId. لو حذفت المصفوفة لعمل بعد كل عرض، ومع setState داخله تدخل حلقة لا تنتهي. ولو تركتها فارغة لما جُلب المستخدم الجديد عند تغيّر المعرّف.
2) الدالة fetch لا ترفض الوعد عند ردود مثل 404 أو 500، وترفضه فقط عند فشل الشبكة نفسها. لذلك نفحص res.ok ونرمي الخطأ بأنفسنا حتى يصل إلى catch.
3) دالة التنظيف التي يرجعها التأثير تعمل قبل التشغيل التالي. إذا اختار المستخدم الحساب 1 ثم الحساب 2 بسرعة، وعاد رد الحساب 1 متأخراً، فإن ignore يكون قد صار true فيُتجاهل الرد القديم. بدون ذلك قد تعرض الشاشة اسماً خاطئاً، وهذا ما يسمى حالة السباق (Race Condition).
في وضع التطوير مع StrictMode سترى الطلب يُرسل مرتين عند أول ظهور: يشغّل React التأثير، ثم ينظّفه، ثم يشغّله مجدداً عمداً، ليتأكد أن التنظيف مكتوب بشكل صحيح. هذا لا يحدث في النسخة المنشورة، وإسكاته بإزالة StrictMode يخفي أخطاء حقيقية بدلاً من إصلاحها.
جلب البيانات داخل Effect يعمل، لكن له حدود واضحة: لا يبدأ إلا بعد تحميل JavaScript وظهور المكوّن، ولا يخزّن النتائج مؤقتاً بين الصفحات، ويسهل أن ينتج سلسلة طلبات متتابعة بطيئة. لهذا يوصي توثيق React باستخدام آلية جلب البيانات في إطار العمل إن وُجد، أو بمكتبة متخصصة مثل TanStack Query في التطبيقات التي تعمل في المتصفح. وفي Next.js، كما سترى في الدرس التاسع، يجلب مكوّن الخادم بياناته مباشرة بـ await دون useEffect. وتذكّر أن أي مفتاح API سري تضعه في كود المتصفح يستطيع أي زائر قراءته من أدوات المطوّر، فمكان المفاتيح السرية هو الخادم.
useEffect للمزامنة مع نظام خارجي بعد الظهور: شبكة، أو مؤقت، أو مكتبة تتعامل مع DOM.
القيم المشتقة تُحسب أثناء العرض، وردود الفعل على النقر مكانها معالج الحدث.
fetch لا يرفض الوعد عند 404 أو 500، فافحص res.ok دائماً.
مخطط
هل يوجد نظام خارجي؟
↓
لا
نعم
↓
احسب في العرض
useEffect
أو معالج حدث
+ دالة تنظيف
مختبر: احذف التأثيرات الزائدة
ابحث في مشروعك عن كل useEffect، واكتب بجانب كل واحد: ما النظام الخارجي الذي يزامنه؟ إن لم تجد إجابة فانقل الكود إلى جسم المكوّن أو إلى معالج حدث، وتأكد أن السلوك لم يتغير.
مهمة عملية
ابنِ مكوّن Profile من الدرس مع ثلاثة أزرار لاختيار المستخدم 1 أو 2 أو 3. افتح تبويب Network واختر سرعة شبكة بطيئة، ثم انقر بسرعة بين الأزرار، وتحقق أن الاسم المعروض يطابق آخر زر نقرته دائماً.
اختبر فهمك (1/3)
مجموع سعر السلة يُحسب من مصفوفة items الموجودة في الحالة. أين تضع الحساب؟
اختبر فهمك (2/3)
لماذا يرجع التأثير الذي يجلب بيانات المستخدم دالة تضبط ignore على true؟
اختبر فهمك (3/3)
رد الخادم بـ 404 لكن الكود دخل then وحاول قراءة JSON. ما التفسير؟
الهدف: تنشئ مشروع Next.js بـ App Router، وتفهم ملفات layout وpage وloading وerror، وتقرر متى يكون المكوّن خادمياً ومتى يحتاج use client.
التطبيق الذي بنيته بـ Vite يعمل كله في المتصفح: يستلم الزائر صفحة HTML شبه فارغة، ثم ينزّل JavaScript الذي يبني المحتوى. هذا مقبول للوحات التحكم الداخلية، لكنه يعني أن المحتوى يتأخر في الظهور، وأنك ستختار بنفسك مكتبة للتوجيه وطريقة لجلب البيانات وأخرى للعرض على الخادم. Next.js إطار عمل مبني على React يقدّم هذه القرارات جاهزة: توجيه مبني على المجلدات، وعرض على الخادم يرسل HTML مكتملاً، وجلب بيانات من الخادم، وتحسينات مدمجة للصور والخطوط.
أنشئ مشروعاً بالأمر الرسمي:
npx create-next-app@latest oasis-academy
cd oasis-academy
npm run dev
في الإصدارات الحالية يسألك المثبّت هل تريد الإعدادات الموصى بها، وتشمل TypeScript وESLint وTailwind CSS وApp Router. اقبلها؛ الأمثلة القادمة مكتوبة بـ TypeScript في أبسط صوره. تحدد صفحة التثبيت في التوثيق حداً أدنى لإصدار Node.js، فإن ظهر خطأ عن الإصدار فحدّث Node من موقعه الرسمي. يعمل خادم التطوير على http://localhost:3000.
ملفات لها معنى خاص داخل مجلد app. في App Router يتعرف Next.js على أسماء ملفات بعينها ويعطي كلاً منها دوراً محدداً:
• layout.tsx: غلاف مشترك يبقى ثابتاً عند التنقل بين الصفحات التي تحته. الـ layout الجذري في app/layout.tsx إلزامي ويجب أن يحتوي وسمي html وbody.
• page.tsx: يجعل المجلد مساراً يمكن زيارته، ومحتواه يُعرض داخل الـ layout.
• loading.tsx: واجهة تحميل فورية؛ يغلّف Next.js الصفحة بحدود Suspense ويعرض هذا الملف حتى تجهز.
• error.tsx: واجهة تظهر عند خطأ غير متوقع في هذا القسم بدلاً من انهيار الموقع كله.
• not-found.tsx: صفحة 404 مخصصة تُعرض لرابط غير موجود أو عند استدعاء notFound().
لاحظ السطر الأول بعد التعليق: حدود الأخطاء في React يجب أن تكون مكوّنات عميل، لذلك يبدأ الملف بـ "use client". واسم دالة إعادة المحاولة تغيّر بين الإصدارات: reset هي المعروفة في Next.js 15، وفي إصدارات 16 الأحدث أضيفت retry وصارت هي الموصى بها، فراجع صفحة error.js في التوثيق لإصدارك قبل النسخ.
مكوّنات الخادم ومكوّنات العميل. كل مكوّن داخل app هو افتراضياً مكوّن خادم (Server Component): يُنفّذ على الخادم، ويمكن أن يكون دالة async تقرأ قاعدة بيانات مباشرة، ولا يُرسل كوده إلى المتصفح. الثمن أنه لا يملك حالة ولا أحداثاً، فلا useState ولا useEffect ولا onClick. حين تحتاجها، تكتب "use client" في أعلى ملف المكوّن، فيصير هذا الملف وكل ما يستورده جزءاً من حزمة المتصفح.
قاعدة القرار: استخدم مكوّن عميل إذا احتجت واحداً من أربعة: حالة، أو معالجات أحداث، أو تأثيرات، أو واجهات المتصفح مثل localStorage. ما عدا ذلك يبقى على الخادم. وضع الحد في أضيق مكان ممكن: زر «أضف للمفضلة» يكون ملف عميل صغيراً، وتبقى الصفحة حوله مكوّن خادم يرسل نصها الطويل HTML جاهزاً.
عند تمرير props من مكوّن خادم إلى مكوّن عميل يجب أن تكون القيم قابلة للتسلسل (Serializable): نصوص وأرقام ومصفوفات وكائنات بسيطة. الدوال العادية لا تعبر هذا الحد، ومن الاستثناءات المهمة دوال الخادم (Server Functions) التي ستستخدمها في النماذج. وإذا استخدمت useState في ملف بلا "use client" فسيتوقف البناء برسالة تشرح أن هذا الخطاف يعمل في مكوّنات العميل فقط، وهي من أكثر الرسائل التي ستراها في أسابيعك الأولى مع Next.js.
create-next-app بالإعدادات الموصى بها ينشئ مشروعاً بـ TypeScript وApp Router.
layout يغلّف ويبقى عند التنقل، وpage يجعل المجلد مساراً، وloading وerror لحالتي الانتظار والخطأ.
المكوّنات في app خادمية افتراضياً، و"use client" يضيف الملف وما يستورده إلى حزمة المتصفح.
ضع حد العميل في أضيق مكوّن يحتاج حالة أو أحداثاً، ومرر له props قابلة للتسلسل.
مخطط
app/
layout.tsx ← غلاف ثابت
page.tsx ← /
loading.tsx ← أثناء الانتظار
error.tsx ← عند الخطأ (عميل)
courses/
page.tsx ← /courses
مختبر: خادم أم عميل؟
أنشئ app/courses/page.tsx كمكوّن خادم يعرض ثلاث دورات من مصفوفة، وأضف FavoriteButton كمكوّن عميل لكل دورة. ثم أضف loading.tsx وerror.tsx للمجلد، وارمِ خطأ عمداً داخل الصفحة لتشاهد واجهة الخطأ.
مهمة عملية
أنشئ مشروع Next.js بالإعدادات الموصى بها، وأضف قسم /courses فيه page وloading وerror ومكوّن عميل واحد. شغّل npm run build واقرأ ملخص المسارات الذي يطبعه في النهاية.
اختبر فهمك (1/3)
أي ملف من ملفات App Router التالية يجب أن يبدأ بالتوجيه "use client"؟
اختبر فهمك (2/3)
صفحة دورة فيها نص طويل وزر «أضف للمفضلة» يغيّر شكله عند النقر. ما التقسيم الأفضل؟
اختبر فهمك (3/3)
استخدمت useState في app/page.tsx دون أي توجيه فتوقف البناء. ما التفسير؟
المجلد الذي لا يحتوي page لا يُنشئ صفحة، فيمكنك استخدامه لتنظيم مكوّنات القسم. وإن أضفت layout.tsx داخل app/courses فسيغلّف صفحات الدورات وحدها، مثل قائمة جانبية تبقى ثابتة أثناء التنقل بينها.
للروابط الداخلية استخدم مكوّن Link من next/link. الوسم a العادي يعيد تحميل المستند كاملاً، أما Link فينتقل داخل التطبيق محافظاً على الـ layout وحالته، ويجلب الصفحات المرتبطة مسبقاً في الإنتاج حين تظهر روابطها على الشاشة، فيبدو التنقل فورياً. للروابط الخارجية يبقى الوسم a هو المناسب. ولتمييز الرابط النشط تحتاج المسار الحالي من الخطاف usePathname، وهذا يجعل شريط التنقل مكوّن عميل. وللانتقال برمجياً بعد عملية ما، كحفظ نموذج، توفر next/navigation الدالة redirect على الخادم، والخطاف useRouter في مكوّنات العميل.
المقطع الديناميكي اسم مجلد بين قوسين مربعين مثل [slug]، وقيمته تصل إلى الصفحة في params. هذه صفحة دورة كاملة تجلب بياناتها على الخادم من واجهة برمجية افتراضية عنوانها api.example.com:
تتبّع طلب زائر للرابط /courses/git من البداية إلى النهاية:
1) يطابق Next.js الرابط مع المجلد [slug]، فتصبح القيمة { slug: "git" }.
2) منذ Next.js 15 أصبحت params وعداً (Promise)، لذلك الصفحة دالة async تكتب await params. وفي مكوّن عميل تقرؤها بالدالة use من React أو بالخطاف useParams.
3) تجلب getCourse البيانات على الخادم مباشرة بـ await، دون useEffect ودون حالة تحميل يدوية. وإن أضفت loading.tsx للمجلد فسيظهر أثناء الانتظار.
4) إذا رد الخادم بـ 404 تستدعي الصفحة notFound()، فيعرض Next.js أقرب ملف not-found. وإذا فشل الطلب لسبب آخر يُرمى خطأ يلتقطه أقرب error.tsx.
5) يصل إلى المتصفح HTML مكتمل فيه العنوان والملخص، ولا يُرسل كود الصفحة نفسه لأنها مكوّن خادم.
إذا كانت قائمة الدورات معروفة وقت البناء، تستطيع تصدير دالة generateStaticParams من الملف نفسه ترجع مصفوفة القيم الممكنة، فيبني Next.js تلك الصفحات مسبقاً كملفات ثابتة سريعة:
ومعاملات الاستعلام بعد علامة الاستفهام، كما في /courses?level=beginner، لا تحتاج مجلداً جديداً. تصل إلى الصفحة في خاصية searchParams، وهي أيضاً وعد منذ Next.js 15، فتكتب const { level } = await searchParams ثم تصفّي القائمة على الخادم. استخدم المقطع الديناميكي حين تمثل القيمة صفحة مستقلة لها عنوانها، كدورة بعينها، ومعاملات الاستعلام حين تمثل طريقة عرض للصفحة نفسها، كفلتر أو ترتيب أو رقم صفحة.
التخزين المؤقت تغيّر بين الإصدارات، فتحقق من إصدارك. في Next.js 14 كانت طلبات fetch على الخادم تُخزَّن مؤقتاً افتراضياً. في Next.js 15 انقلب الافتراض، فصارت لا تُخزَّن ما لم تطلب ذلك صراحة بالخيار cache: "force-cache"، أو بالخيار next: { revalidate: 3600 } الذي يعيد جلب البيانات بعد عدد الثواني المحدد كما في المثال. ثم قدّم Next.js 16 نموذجاً اختيارياً اسمه Cache Components يُفعَّل بالخيار cacheComponents في ملف الإعداد، ويعتمد على التوجيه "use cache". لذلك افتح package.json لترى رقم إصدار next في مشروعك، ثم اقرأ صفحة التخزين المؤقت المطابقة له في التوثيق قبل أن تعتمد على أي سلوك افتراضي، فالمقالات القديمة على الإنترنت تصف غالباً سلوكاً لم يعد قائماً.
المجلد مع page.tsx يساوي مساراً، وlayout داخل مجلد يغلّف صفحات ذلك القسم فقط.
Link ينتقل داخل التطبيق ويجلب الصفحات مسبقاً، والوسم a للروابط الخارجية.
في المقطع [slug] تصل القيمة في params، وهي Promise منذ Next.js 15 فتُقرأ بـ await.
سلوك التخزين المؤقت يختلف بين 14 و15 و16؛ راجع إصدار next في package.json والتوثيق المطابق.
أنشئ /courses بقائمة روابط، و/courses/[slug] تقرأ slug وتعرضه. بدلاً من الواجهة الافتراضية استخدم مصفوفة محلية للدورات، واستدعِ notFound() حين لا يوجد slug مطابق، ثم جرّب رابطاً غير موجود.
مهمة عملية
ابنِ قسم دورات فيه صفحة قائمة وصفحة ديناميكية لكل دورة وnot-found مخصصة وشريط تنقل يميّز الرابط النشط. شغّل npm run build وتحقق في ملخص البناء أي المسارات بُنيت ثابتة وأيها ديناميكية.
اختبر فهمك (1/3)
أي بنية ملفات تنشئ صفحة لكل دورة مثل /courses/git و/courses/react؟
اختبر فهمك (2/3)
لماذا تُكتب صفحة المقطع الديناميكي دالة async مع await params في Next.js 15 وما بعده؟
اختبر فهمك (3/3)
مشروع على Next.js 15 يجلب بيانات بـ fetch دون أي خيار تخزين. ما السلوك الافتراضي للطلب؟
الهدف: تسلّم صفحة هبوط عربية في Next.js باتجاه RTL وخط عربي وبيانات وصفية ونموذج تواصل يعمل عبر Server Action، ثم تبنيها وتنشرها.
المشروع صفحة هبوط لمنصة تعليمية افتراضية اسمها «الواحة». المطلوب قسم رئيسي (Hero) بعنوان ووصف وزر دعوة، وثلاث بطاقات ميزات تُعرض بـ map من مصفوفة بيانات، وأسئلة شائعة تنفتح وتنغلق، ونموذج تواصل يصل إلى الخادم. ستجمع هنا كل ما سبق، وتضيف ثلاثة أشياء تخص المواقع الحقيقية: الاتجاه العربي، والبيانات الوصفية لمحركات البحث، والنشر.
app/layout.tsx ← html و body والخط والبيانات الوصفية
app/page.tsx ← مكوّن خادم يجمع الأقسام
app/actions.ts ← "use server": دالة إرسال النموذج
components/Hero.tsx
components/FeatureCard.tsx
components/Faq.tsx ← "use client"
components/ContactForm.tsx ← "use client"
ملفان فقط يحتاجان "use client": الأسئلة الشائعة لأنها تحتفظ بحالة الفتح، ونموذج التواصل لأنه يعرض حالة الإرسال ورسائل النتيجة. بقية الصفحة تصل إلى الزائر HTML جاهزاً.
المرحلة الأولى: الاتجاه والخط والبيانات الوصفية في الـ layout الجذري.
الخاصية lang="ar" تخبر المتصفح ومحركات البحث وقارئات الشاشة بلغة الصفحة، فتُنطق بالعربية وتُختار لها الخطوط المناسبة. والخاصية dir="rtl" تقلب اتجاه الكتابة وترتيب العناصر في التخطيطات المرنة والشبكية. وضعهما على وسم html أفضل من توزيعهما على عناصر متفرقة، كما توصي إرشادات W3C. وفي CSS استخدم الخصائص المنطقية مثل margin-inline-start وpadding-inline-end بدلاً من left وright، فتعمل في الاتجاهين دون تعديل. أما next/font فتستضيف ملفات الخط مع موقعك، فلا يطلبها المتصفح من خوادم خارجية عند كل زيارة.
كائن metadata المُصدَّر يولّد وسمي title وmeta description في رأس الصفحة، ويعمل في مكوّنات الخادم فقط. ولصفحات المقاطع الديناميكية استخدم الدالة generateMetadata، التي تستقبل params وترجع عنواناً ووصفاً مختلفين لكل دورة.
المرحلة الثانية: نموذج التواصل عبر Server Action. دالة الخادم تعيش في ملف يبدأ بالتوجيه "use server":
// app/actions.ts
"use server";
export type ContactState = { ok: boolean; message: string };
export async function sendMessage(
prevState: ContactState,
formData: FormData
): Promise<ContactState> {
const email = String(formData.get("email") ?? "").trim();
const text = String(formData.get("message") ?? "").trim();
if (!email.includes("@") || text.length < 10) {
return { ok: false, message: "تحقق من البريد واكتب رسالة من 10 أحرف على الأقل" };
}
// احفظ الرسالة في قاعدة بيانات أو أرسلها عبر خدمة بريد هنا
return { ok: true, message: "وصلت رسالتك، شكراً لك" };
}
ثم مكوّن النموذج، وهو مكوّن عميل يستخدم الخطاف useActionState من React 19:
يرجع الخطاف ثلاثة أشياء: آخر حالة أعادتها الدالة، ودالة تمررها إلى action، وقيمة منطقية تكون true أثناء الإرسال لتعطيل الزر. وحين يُستخدم useActionState تستقبل دالة الخادم الحالة السابقة معاملاً أول قبل FormData. نقطة أمان أساسية: دالة الخادم يمكن استدعاؤها بطلب POST مباشر دون المرور بنموذجك، لذلك تحقق من كل مدخل داخلها كأنه قادم من مجهول، وأضف التحقق من هوية المستخدم قبل أي عملية حساسة. خاصية required في المتصفح تحسّن تجربة المستخدم، لكنها لا تحمي الخادم من شيء.
المرحلة الثالثة: البناء والنشر. نفّذ npm run build أولاً؛ أخطاء الأنواع واستخدام خطاف في مكوّن خادم تظهر هنا قبل أن يراها الزوار. ثم اختر طريقة النشر:
• منصة تدعم Next.js مباشرة مثل Vercel، الشركة التي تطوّر Next.js: تربط مستودع Git فيُبنى الموقع مع كل push، وتحصل على رابط معاينة لكل فرع. خيار سريع للبداية، وتوجد منصات أخرى تدعم الإطار.
• خادم Node.js خاص بك أو حاوية Docker: تنفّذ npm run build ثم npm start، وتدعم هذه الطريقة ميزات Next.js كلها.
• تصدير ثابت بالخيار output: "export" في ملف الإعداد: ينتج ملفات HTML تُرفع إلى أي استضافة ثابتة، لكنه لا يدعم الميزات التي تحتاج خادماً، ومنها Server Actions، فلن يعمل نموذج التواصل بهذه الطريقة.
معايير القبول قبل التسليم:
✓ npm run build ينجح دون أخطاء أو تحذيرات أنواع.
✓ الصفحة باتجاه RTL كامل، والأسهم والأيقونات الاتجاهية معكوسة حيث يلزم.
✓ نموذج التواصل يرفض مدخلاً قصيراً برسالة عربية واضحة، ويقبل المدخل الصحيح.
✓ عنوان الصفحة ووصفها ووسم html بخاصيتيه تظهر عند فتح مصدر الصفحة في المتصفح.
✓ ملف README يشرح التشغيل والبناء خطوة بخطوة.
lang="ar" وdir="rtl" على وسم html، مع خصائص CSS المنطقية بدلاً من left وright.
metadata وgenerateMetadata للعنوان والوصف، وتعملان في مكوّنات الخادم فقط.
Server Action مع useActionState يرسل النموذج إلى الخادم، والتحقق من المدخلات داخل الدالة إلزامي.
npm run build قبل أي نشر، والتصدير الثابت لا يدعم Server Actions.
مخطط
layout (ar, rtl, خط, metadata)
↓
page (خادم)
↓
Hero←Features←Faq (عميل)
↓
ContactForm (عميل)←actions.ts (خادم)
↓
build←نشر
مختبر: مراجعة قبل النشر
افتح الصفحة على هاتف حقيقي، وجرّب النموذج بمدخل خاطئ ثم صحيح، وافتح مصدر الصفحة للتأكد من العنوان والوصف ووسم html. اطلب من شخص لم يرَ المشروع أن يشرح ما تقدمه المنصة بعد 30 ثانية من فتح الصفحة.
مهمة عملية
سلّم مستودع صفحة الهبوط كاملاً بالمكوّنات المقترحة مع README عربي، وانشره على منصة أو خادم من اختيارك، ثم أرفق الرابط وناتج npm run build.
اختبر فهمك (1/3)
أين يوضع اتجاه الصفحة العربية في مشروع Next.js بالشكل الموصى به؟
اختبر فهمك (2/3)
لماذا يجب التحقق من المدخلات داخل دالة Server Action رغم وجود required في النموذج؟
اختبر فهمك (3/3)
نشرت الموقع بالخيار output: "export" على استضافة ثابتة فتوقف نموذج التواصل. لماذا؟