SELFLAB الستاك الكامل · ١١ إقليم SYSTEM ONLINE
الإقليم 08منهجٌ يُدرَس وحدك

الإقليم ٨ — React: العميل يُغلق الدائرة

يبني فوق: الإغلاق (١)، رسالةُ HTTP والمظروف والأفعال (٠)، CORS والأصل (٣)، حملُ الـ JWT في Authorization (٧)، وثنائيّةُ ثابت/ديناميكيّ من بنية الويب. النطاق المتّفَق عليه: بقدرِ ما يُغلق الدائرة ويكشف تدفّقَ البيانات — لا غوصٌ في دواخل React. يفتح: التدفّقُ الكامل (٩)، وواجهةُ التصحيح (١٠).

النبذة

بنينا الخادمَ كاملاً، لكنّ المتدرّبَ لا يكلّم curl — يكلّم شاشة. React هو العميلُ الذي يولّد كلَّ الطلبات التي أتقنتها (٠)، يحمل الـ JWT في كل واحدة (٧)، ويأخذ المظروفَ العائدَ فيرسمه. هدفُنا هنا محدّدٌ بقرارك: نفهم React بقدرِ ما يكشف تدفّقَ البيانات ويُغلق الدائرة من المتصفّح إلى Postgres ورجوعاً — لا أكثر. لن نغوص في دواخله؛ نمسك فكرتَه المركزيّة وكيف يتكلّم API، فتصير قادراً على إدارة هذا الطرف بثقة.


اللغز المستفزّ

اصنع شاشةً تعرض قائمةَ المناهج من GET /api/v1/curricula، وفيها نموذجُ تسجيل. عقليّةُ C/الأمريّة تقفز للطريقة المباشرة: اعبث بالـ DOM بيدك:

js
const res = await fetch("/api/v1/curricula"); const { data } = await res.json(); const ul = document.getElementById("list"); data.forEach((c) => { const li = document.createElement("li"); li.textContent = c.title; ul.appendChild(li); // ارسم كلَّ عنصرٍ يدوياً });

اشتغل — حتى تنمو الحالة. أضِف: مؤشّرَ تحميل، رسالةَ خطأ، زرَّ تحديثٍ يعيد الجلب، حقولَ النموذج، أخطاءَ تحقّقٍ بجانب كل خانة، وحالةَ "مسجَّلُ الدخول". الآن لكل تغيّرٍ في البيانات يجب أن تجد العقد الصحيحة في DOM وتحدّثها بيدك: تمسح القائمةَ القديمةَ قبل الجديدة (نسيتَ؟ تتضاعف)، تُظهر/تخفي التحميل، تضيف/تزيل رسائلَ الخطأ. يصير كودُك متاهةَ "أبقِ DOM متزامناً مع البيانات يدوياً"، وكلُّ نسيانٍ خطأٌ بصريّ.

تذكّر شذوذَ التحديث في الإقليم ٤ (حقيقةٌ مبعثرةٌ تتعفّن حين تنسى تحديثَ نسخة)؟ هذا نظيرُه في الواجهة: حقيقةُ الحالة في مكانٍ (متغيّراتك)، ونسختُها في مكانٍ آخر (DOM)، وأنت مسؤولٌ يدوياً عن تطابقهما — فيتباعدان.

السؤال المستفزّ:

ملاحظة

أبقِ الشاشةَ متطابقةً تماماً مع بياناتٍ متغيّرة، دون أن تلمس DOM بيدك أبداً. كيف؟


ليش: الفكرة المركزيّة — الواجهةُ دالّةٌ في الحالة

عكسُ العبث اليدويّ. بدل "حين تتغيّر البيانات، جِد العقد وعدّلها" (أمريّ)، تقول: **"هكذا تبدو الشاشةُ لهذه البيانات"** (تصريحيّ) — تكتب دالّةً من الحالة إلى وصفِ الواجهة، وReact يتكفّل بجعل DOM يطابق الوصف:

UI = f(state) تغيّرتِ الحالة → أعِد تشغيل f → احصل على وصفٍ جديد → React يطبّق الفرقَ الأدنى على DOM الحقيقيّ

أنت لا تحدّث DOM. تغيّر الحالةَ وتعيد الوصف، وReact يحسب التغييرَ الأدنى ويطبّقه. هذا يقتل التباعدَ من جذره: لا توجد نسختان لتتزامنا — توجد حالةٌ واحدةٌ، والشاشةُ اشتقاقٌ منها دائماً.

نموذج ذهني

النموذج الذهني (هذا React كلّه): اكتب ماذا يجب أن تبدو الشاشةُ لكلِّ حالةٍ ممكنة (تحميل، خطأ، قائمة، فارغ)، لا كيف تنتقل بينها. التنقّلُ مشكلةُ React لا مشكلتُك. هذا التحرّرُ هو لماذا وُجد.

كيف يطابق DOM بكفاءة (تصالحٌ — Virtual DOM، باختصار)

تعديلُ DOM الحقيقيّ مباشرةً مكلفٌ ومطوَّل. فReact يحتفظ بـوصفٍ خفيفٍ للواجهة في الذاكرة (Virtual DOM)؛ حين تتغيّر الحالة يعيد تشغيلَ دالّتك، يقارن الوصفَ الجديدَ بالقديم (reconciliation)، ويطبّق فقط الفروق على DOM الحقيقيّ. النتيجة: تكتب كأنك ترسم الشاشةَ كاملةً كلَّ مرّة، وينفّذ هو الحدَّ الأدنى. (تفاصيلُ الخوارزميّة خارجَ نطاقنا المتّفق عليه — يكفيك لماذا توجد: أتمتةُ "ما الذي تغيّر".)


كيف: المكوّنات، الـ props، و JSX

المكوّن دالّةٌ تأخذ مدخلاتٍ (props — كوسائط الدالة) وتُعيد وصفَ واجهة:

jsx
function CurriculumCard({ title, learners }) { // props = مدخلات return <li>{title} — {learners} متدرّب</li>; // JSX = وصفُ الواجهة }

JSX صياغةٌ تُشبه HTML داخل JS، تُترجَم إلى نداءات دوالٍّ تبني الوصف (ليست HTML — سكّرٌ فوق JS، يُمحى كالأنواع تقريباً). والمكوّناتُ تتركّب: مكوّنٌ يحوي مكوّنات (كتركيب middleware في ٣، وكتداخل بيانات الـ ERD في ٤):

jsx
function CurriculaList({ items }) { return <ul>{items.map((c) => <CurriculumCard key={c.id} title={c.title} learners={c.learners} />)}</ul>; }

props تتدفّق للأسفل فقط (اتّجاهٌ واحد — كاتّجاه المفتاح الأجنبيّ في ٤؛ للبياناتِ وجهةٌ تسهّل التتبّع). وkey يساعد التصالحَ على تمييز عناصر القائمة.


كيف: الحالة والـ hooks (حيث تثمر إغلاقاتُ الإقليم ١)

useState — قيمةٌ تبقى وتُطلِق إعادةَ الرسم

jsx
function Counter() { const [count, setCount] = useState(0); // قيمةٌ تبقى بين عمليات الرسم return <button onClick={() => setCount(count + 1)}>{count}</button>; }

useState يعطيك قيمةً تبقى عبر عمليات إعادة الرسم ودالّةً لتغييرها؛ ونداءُ setCount يُطلِق إعادةَ رسمٍ (يعيد تشغيلَ المكوّن، فيظهر العدّ الجديد). من أين "تبقى" القيمةُ بين عمليات الرسم والدالّةُ تُعاد كلَّ مرّة؟ من الإغلاق (١): تذكّر makeCounter و"أين يعيش count" — نفسُ الآليّة بالضبط. React يربط الحالةَ بموضع المكوّن، والإغلاقُ يلتقطها. (لهذا أصرّ الإقليم ١ على فهمِ الإغلاق من جذره — هنا يُدفَع الثمن.)

useEffect — الآثارُ الجانبيّة (مثل جلب البيانات) بعد الرسم

الرسمُ يجب أن يكون نقيّاً (دالّةٌ من حالةٍ لوصف، بلا آثار). لكن جلبَ البيانات أثرٌ جانبيّ. useEffect يفصله: نفّذ شيئاً بعد الرسم، وتحكّم بـمتى عبر مصفوفة التبعيّات:

jsx
useEffect(() => { fetchData(); }, []); // [] = مرّةً واحدةً عند الترکيب (mount). [x] = كلّما تغيّر x.

دورةُ الحياة تكتمل: تغيُّرُ حالةٍ → إعادةُ رسمٍ → تصالحٌ → تحديثُ DOM؛ والآثارُ (جلب) تجري بعد الرسم وتغيّر الحالةَ فتعيد الدورة.


كيف: جلبُ البيانات — هنا تُغلَق الدائرة فعلاً

كلُّ ما بنيتَه في الخادم يلتقيه العميلُ هنا:

jsx
function Curricula() { const [items, setItems] = useState([]); const [status, setStatus] = useState("loading"); // loading | error | ready useEffect(() => { fetch(`${import.meta.env.VITE_API_URL}/curricula`, { // العنوانُ من البيئة headers: { Authorization: `Bearer ${localStorage.getItem("token")}` }, // الـ JWT (٧) }) .then((r) => r.json()) // المظروف {success, data} (٠) .then((env) => { setItems(env.data); setStatus("ready"); }) .catch(() => setStatus("error")); }, []); if (status === "loading") return <p>...جارٍ التحميل</p>; // الواجهةُ دالّةٌ في الحالة: if (status === "error") return <p>تعذّر الجلب</p>; // كلُّ حالةٍ ووصفُها return <CurriculaList items={items} />; }

لاحظ كم خيطاً يلتقي في هذه الدالّة الواحدة:

النماذجُ المضبوطة (controlled inputs) — وإغلاقُ حلقة التحقّق

jsx
function RegisterForm() { const [form, setForm] = useState({ username: "", email: "", password: "" }); const [errors, setErrors] = useState({}); async function submit(e) { e.preventDefault(); const res = await fetch(`${import.meta.env.VITE_API_URL}/auth/register`, { method: "POST", headers: { "Content-Type": "application/json" }, // النوع (٠) body: JSON.stringify(form), // الجسم (٠) }); const env = await res.json(); if (!res.ok) return setErrors(env.details || {}); // 400 + fieldErrors من Zod (٦) /* نجاح: 201 — وجّه لتسجيل الدخول */ } return ( <form onSubmit={submit}> <input value={form.username} // القيمةُ مربوطةٌ بالحالة onChange={(e) => setForm({ ...form, username: e.target.value })} /> {errors.username && <span>{errors.username[0]}</span>} {/* خطأُ الحقل من الخادم */} {/* ...email, password... */} <button>تسجيل</button> </form> ); }

الخانةُ مضبوطةٌ: قيمتُها من الحالة، وتغييرُها يحدّث الحالة — مصدرُ حقيقةٍ واحد. وعند الإرسال، أخطاءُ 400 التي يردّها Zod (٦) تظهر بجانب كل خانة. هذا هو إغلاقُ حلقة التحقّق بعينه: البوّابةُ التي بنيتَها في الخادم تُعرَض للمستخدمِ في العميل.

حفظُ الرمز (مقايضةُ ٧ مجدّداً)

بعد login الناجح، تحفظ الـ JWT لترسله لاحقاً. localStorage بسيطٌ لكن معرّضٌ لسرقةِ XSS؛ كوكي httpOnly أأمنُ لكن يحتاج CSRF (٧). قرارٌ تملكه بفهمِ مقايضته.


ليش: Vite والبناءُ الثابت (وصلةٌ ببنية الويب)

Vite أداةُ مشروعك للتطوير والبناء (المنفذ 5173، VITE_API_URL): خادمُ تطويرٍ سريعٌ بإعادةِ تحميلٍ ساخن (HMR)، وbuild يحوّل React إلى أصولٍ ثابتة (HTML/JS/ CSS). تذكّر ثنائيّةَ بنية الويب: ثابت ≠ ديناميكيّ. React المبنيُّ ثابتٌ يخدمه أيُّ web server (Nginx)؛ والـ API هو الجزءُ الديناميكيّ. الفصلُ الذي تعلّمتَه نظرياً يتجسّد هنا: ملفّاتٌ ثابتةٌ في المتصفّح، تكلّم خادماً ديناميكياً عبر HTTP. (شاشاتُ مشروعك الأساسيّة من README: مصادقة، متصفّحُ مناهج، مصمّمُ المنهج المتداخل، وواجهةُ التعلّم + التصحيح.)


اللغز / البناء من الصفر

أدواتك: مشروعُ React (npm create vite@latest -- --template react)، وخادمُك من الأقاليم ٣–٧ (أو خادمٌ بسيطٌ يردّ مظاريفَ).

اللغز أ — اعِش الألمَ الأمريّ أولاً (بلا React). بصفحة HTML + JS خام، اجلب قائمةً وارسمها بـ DOM يدويّ، ثم أضِف: مؤشّرَ تحميل، زرَّ تحديثٍ يعيد الجلب، ورسالةَ خطأ. لاحظ يدوياً كم موضعاً يجب أن تتذكّر تحديثَه، وافتعِل خطأَ "نسيتُ مسحَ القائمة القديمة" وشاهد التضاعف. اكتب بجملة: كيف يشبه هذا شذوذَ التحديث في الإقليم ٤؟

اللغز ب — أعِد بناءها في React، بلا لمسِ DOM. اكتب <Curricula/> بـ useState + useEffect + fetch، يعرض حالاتِ loading/error/ready كلَّ واحدةٍ بوصفها. القيد الحاكم: ممنوعٌ منعاً باتاً document.getElementById/appendChild/ innerHTML في نسخة React — تغيّر الحالةَ فقط. راقب الشاشةَ تبقى متزامنةً تلقائياً. اشعر بالفرق بين (أ) و(ب).

اللغز ج — أغلق حلقة التحقّق. اكتب <RegisterForm/> بخاناتٍ مضبوطة، يرسل POST بمظروفِ JSON، وإن ردّ الخادمُ 400 يعرض fieldErrors (Zod، ٦) بجانب كل خانة. أرسِل بياناتٍ مخالفةً (كلمةٌ قصيرة، بريدٌ مشوّه) وشاهد رسائلَ خادمِك تظهر في الواجهة. هذا التحقّقُ الذي بنيتَه في ٦، يُغلَق بصرياً.

اللغز د — تتبّع الأسلاك بعينك. افتح DevTools › Network، وانقر في واجهتك. شاهد الطلباتِ الحقيقيّة التي تولّدها نقراتُك: الفعلَ، المسار، ترويسةَ Authorization: Bearer، Content-Type، طلبَ CORS التمهيديّ (preflight OPTIONS) إن ظهر، والمظروفَ العائد. هذا هو الإقليم ٠ حياً: ما كتبتَه بأصابعك في nc صار ما تولّده نقرةٌ. صف المسار: نقرة → fetch → HTTP → الخادم → ... → ردّ → setState → إعادةُ رسم.

اللغز هـ — الجلسة الكاملة. اربط: <Login/> (POST → احفظ الـ JWT) ثم <Me/> (جلبُ /me بترويسة Bearer، يعرض اسمك) ثم تسجيلُ خروجٍ (امسح الرمز). هذا مصغّرُ تدفّقِ مصادقةِ مشروعك من جهة العميل، يغلق ما بنيتَه في ٧.

ملاحظة

لا تنتقل قبل أن تبني (ب) دون لمسِ DOM وتتتبّع الأسلاك في (د). الأول يثبّت "الواجهةُ دالّةٌ في الحالة"، والثاني يجعلك ترى الستاكَ كلَّه يعمل عبر السلك.


الخلاصة — أين تتّصل هذه العقدة بالشجرة

  • تحت: React يحلّ تباعدَ الواجهة عن البيانات (نظيرُ شذوذِ ٤) بقلبِ النموذج: UI = f(state) تصريحيّةً بدل عبثِ DOM أمريّ. مكوّناتٌ تتركّب، props تتدفّق لأسفل، وuseState/useEffect يقفان على الإغلاق (١). وfetch يولّد رسالةَ HTTP (٠) حاملاً الـ JWT (٧)، يفكّ المظروف (٠)، عبر CORS (٣)، والبناءُ ثابتٌ (بنية الويب).
  • العقدة الجديدة: العميلُ التصريحيّ — الطرفُ الذي يولّد كلَّ الطلبات ويعرض كلَّ الردود، فيُغلق نصفَ الدائرة الذي كان مفقوداً.
  • فوق: كلُّ شاشةٍ في مشروعك (متصفّحُ المناهج، المصمّم، واجهةُ التصحيح) ليست إلا مكوّناتٍ تجلب endpoints بنيتَها. وواجهةُ التصحيح (١٠) مكوّنٌ يرسل كودَ المتدرّب ويعرض النتيجة. والآن نملك الطرفين — نجمع الرحلةَ كلَّها (٩).

الأبواب المفتوحة: صار عندنا الطرفان (عميلٌ + خادمٌ + قاعدة). كيف تسري بايتةٌ واحدةٌ من نقرةِ المتدرّب حتى صفٍّ في Postgres ورجوعاً، عبر كلِّ الحلقات التي بنيتَها؟ (٩). وكيف يشغّل الخادمُ كودَ C الذي يرسله المتدرّب بأمان؟ (١٠).


الوصلة للواقع (نمط SelfLab)

frontend/README.md يصير خريطةً تقرؤها: "يخزّن الـ JWT بعد الدخول ويرسله Authorization: Bearer" (٧)، "يكلّم /api/v1 من VITE_API_URL" (البيئة + ٠)، والشاشات (مصادقة، متصفّحُ مناهج، مصمّمٌ متداخل، تعلّم + تصحيح) = مكوّناتٌ تجلب endpoints. ومجلّد frontend/ فارغٌ إلا README — فهذا الطرفُ كلُّه شيءٌ تبنيه وتملكه، وصرتَ تعرف كيف يتكلّم خادمَك بالضبط: المظروف، الترويسات، CORS، الرمز.

بذرة الإقليم التالي

البذرة التالية: نملك الطرفين. حان أن نتتبّع بايتةً واحدةً في رحلتها الكاملة — من onClick في React حتى INSERT في Postgres ورجوعاً إلى setState وإعادةِ الرسم — عبر كلِّ حلقةٍ بنيتَها بيدك. ننزل للإقليم ٩، السنثيسس: حيث تتجمّع كلُّ النقاط في صورةٍ واحدةٍ واضحة، وهي الصورةُ التي طلبتَها من أول يوم: "أعرف كيف تتدفّق البيانات بوضوح".

الستاك الكامل · من باب الخادم إلى داخله ليش قبل كيف · صُمّم لـ يزيد