React: بناء واجهات مستخدم حديثة
خروج

الدرس 1 من 13 · أساسيات React

لماذا React: المكوّنات، والعرض التصريحي، وأول مشروع باستعمال Vite

40 د Node.js 22 (Debian, git, npm, python3)

الهدف

أن تفهم المشكلة التي يحلّها React، وكيف يحدّث الشاشة، وأين يقع ضمن منظومته (Vite وNext.js وReact Router)، ثم أن تبني يدويًا بنية مشروع Vite لتعرف ما يحتويه كل ملف.

أهم النقاط

  • React مكتبة JavaScript لبناء الواجهات انطلاقًا من مكوّنات (components): دوالّ تُرجع ترميزًا (markup).
  • العرض تصريحي (declarative): تصف الشاشة لحالة معيّنة، ويتكفّل React بتعديل الـ DOM عندما تتغيّر الحالة.
  • يعمل React على مرحلتين: العرض (render)، حيث يستدعي مكوّناتك لحساب النتيجة، ثم التثبيت (commit)، حيث لا يطبّق على الـ DOM إلا الفروق.
  • لا يتكفّل React «إلا» بالواجهة. أما الأدوات، فنستعمل Vite (خادم التطوير والبناء) أو إطار عمل (framework) مثل Next.js أو React Router.
  • يتكوّن مشروع Vite من أربع قطع: package.json وindex.html وsrc/main.jsx (نقطة الدخول) وsrc/App.jsx (أول مكوّن).

المشكلة: تحديث الشاشة يدويًا

لنأخذ تعاونية الزرابي Tifawin بأزيلال، التي تبيع عبر الإنترنت. في دورة «Web»، كتبت هذا النوع من الشيفرة لعرض عدد السلع في السلة:

// JavaScript «أمرية» (impérative): نخبر المتصفح بما يجب تغييره، خطوةً بخطوة
let nombre = 0;
const badge = document.querySelector('#badge-panier');
const bouton = document.querySelector('#ajouter');

bouton.addEventListener('click', () => {
  nombre = nombre + 1;
  badge.textContent = nombre;                 // لا تنسَ تحديث الشارة…
  if (nombre > 0) badge.classList.remove('cache');  // …وظهورها…
  // …والمجموع، وزر «Commander»، والملخّص أسفل الصفحة…
});

ما دامت الشاشة تضم ثلاثة عناصر، فكل شيء على ما يرام. لكن مع كتالوج ومرشّحات وسلة واستمارة طلب، يجب على كل حدث أن يتذكّر جميع المناطق التي ينبغي تحديثها. والسهو يتحوّل إلى أخطاء (bugs): الشارة تعرض 3، والملخّص يعرض 2.

جواب React: أن تصف، لا أن تعدّل

مع React، تكتب ما يجب أن تكون عليه الشاشة لقيمة معيّنة. وعندما تتغيّر القيمة، يعيد React الحساب ويحدّث الـ DOM نيابةً عنك:

import { useState } from 'react';

export default function BoutonPanier() {
  const [nombre, setNombre] = useState(0);   // الحالة: «ذاكرة» المكوّن

  return (
    <div>
      <button onClick={() => setNombre(nombre + 1)}>Ajouter au panier</button>
      {nombre > 0 && <span className="badge">{nombre}</span>}
    </div>
  );
}

لم يعد هناك querySelector ولا textContent: الواجهة أصبحت دالةً للحالة. ويمكن تلخيص ذلك في صيغة: الواجهة = f(الحالة).

المكوّنات: لبنات قابلة لإعادة الاستعمال

المكوّن دالة JavaScript يبدأ اسمها بحرف كبير وتُرجع ترميزًا (من نوع JSX، الذي سنراه في الدرس الموالي). نجمّعها كما نجمّع اللبنات:

function CarteProduit({ nom, prix }) {
  return (
    <article>
      <h2>{nom}</h2>
      <p>{prix} DH</p>
    </article>
  );
}

export default function Catalogue() {
  return (
    <main>
      <CarteProduit nom="Tapis Azilal 2 x 3 m" prix={4200} />
      <CarteProduit nom="Coussin en cuir de Fès" prix={350} />
    </main>
  );
}

يصبح التطبيق بأكمله شجرةً من المكوّنات: App يحتوي على Entete وCatalogue وPanier؛ وCatalogue يحتوي على عدة CarteProduit، وهكذا.

العرض والتثبيت: ما يُسمّى «DOM الافتراضي»

كثيرًا ما نسمع أن React يستعمل «DOM افتراضيًا» (virtual DOM). عمليًا، يجري كل تحديث على ثلاث مراحل:

  1. الإطلاق (trigger): عند بدء التشغيل (العرض الأولي)، أو عندما تتغيّر حالة ما (setNombre(...)).
  2. العرض (render): يستدعي React مكوّناتك. فتُرجع وصفًا خفيفًا للواجهة (كائنات JavaScript، وليست عُقد DOM حقيقية). ثم يقارن React هذا الوصف بوصف العرض السابق: هذه هي المطابقة (reconciliation).
  3. التثبيت (commit): يطبّق React على الـ DOM الحقيقي الفروق فقط. فإذا تغيّرت الشارة وحدها، فلن يُعدَّل إلا نصّ الشارة.

النتيجة العملية: حقل الإدخال الذي يكتب فيه المستخدم نصًا لا «يُمسَح» بسبب عرض جديد، لأن React لا يلمس ما لم يتغيّر.

setNombre(3)
   │
   ▼
Rendu  : BoutonPanier() est rappelé → <span>3</span> au lieu de <span>2</span>
   │
   ▼
Commit : un seul changement dans le DOM, le texte du <span>
   │
   ▼
Le navigateur repeint l'écran

(Rendu = العرض: يُستدعى BoutonPanier() من جديد فيُرجع <span>3</span> بدلًا من <span>2</span>؛ Commit = التثبيت: تغيير واحد فقط في الـ DOM هو نصّ الـ <span>؛ ثم يعيد المتصفح رسم الشاشة.)

تذكّر: «العرض» (render) يعني استدعاء مكوّناتك، وليس بالضرورة لمس الـ DOM.

المنظومة: React لا يأتي وحده

يتكفّل React بالواجهة. لكن التطبيق الحقيقي يحتاج أيضًا إلى تحويل JSX، وتجميع الملفات، وتدبير الصفحات، وأحيانًا إلى العرض من جهة الخادم. لذلك نختار أداةً تحيط به:

الأداة الدور متى نختارها
Vite خادم تطوير سريع جدًا، وبناء نسخة الإنتاج تطبيق «من جهة العميل» (SPA): لوحة قيادة، واجهة إدارة (back-office)، متجر يُقدَّم على شكل ملفات ثابتة
Next.js إطار عمل متكامل: صفحات، عرض من جهة الخادم، مسارات API موقع عمومي يجب أن يكون جيّد الظهور في محركات البحث، عرض من جهة الخادم، مشروع كبير
React Router التنقّل بين الصفحات؛ ويمكن أن يُستعمل أيضًا إطارَ عمل متكاملًا SPA متعدد الصفحات (الدرس 2.5)

في هذا التكوين، نستعمل Vite: فهو الأبسط للتعلّم، والنتيجة (ملفات HTML وCSS وJS) يمكن نشرها على أي خادم nginx.

يمكن أيضًا إضافة React إلى جزء من موقع موجود (أداة حجز (widget) داخل صفحة PHP مثلًا): وفي هذه الحالة نركّب React في عنصر واحد فقط من الصفحة.

إنشاء مشروع Vite (مع اتصال بالشبكة)

على جهازك، مع Node.js 20 أو إصدار أحدث (انظر دورة «Node.js»):

npm create vite@latest boutique -- --template react   # يولّد المشروع
cd boutique
npm install                    # يحمّل react وreact-dom وvite…
npm run dev                    # خادم التطوير على http://localhost:5173

في كل مرة تحفظ فيها ملفًا، تتحدّث الصفحة دون إعادة تحميل (إعادة التحميل الساخن (hot reload)).

ما يحتويه المشروع

boutique/
├── package.json        # التبعيات والسكربتات
├── index.html          # صفحة HTML الوحيدة: div فارغ + سكربت الدخول
├── vite.config.js      # إعدادات Vite (إضافة React)
├── public/             # ملفات تُنسخ كما هي (favicon وrobots.txt)
└── src/
    ├── main.jsx        # نقطة الدخول: تركّب React داخل الـ div
    ├── App.jsx         # المكوّن الجذر
    └── index.css

package.json

{
  "name": "boutique-artisanat",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^19.1.0",
    "react-dom": "^19.1.0"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^5.0.0",
    "vite": "^7.0.0"
  }
}
  • يحتوي react على المكوّنات وJSX والـ hooks؛ أما react-dom فيعرف كيف يعرض داخل المتصفح.
  • لا يُستعمل vite وإضافته إلا للتطوير والبناء: لذلك فهما ضمن devDependencies.

index.html

<!doctype html>
<html lang="fr">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Coopérative Tifawin</title>
  </head>
  <body>
    <div id="root"></div>                                  <!-- هنا سيعرض React كل شيء -->
    <script type="module" src="/src/main.jsx"></script>    <!-- يحوّل Vite شيفرة JSX -->
  </body>
</html>

src/main.jsx

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>
);

يتولّى createRoot التحكم في div#root؛ ويعرض فيه render المكوّن App. أما StrictMode فيفعّل فحوصات إضافية أثناء التطوير فقط (فهو مثلًا يستدعي بعض الشيفرات مرتين لكشف الأخطاء).

src/App.jsx

export default function App() {
  return <h1>Coopérative Tifawin</h1>;
}

تهيئة جهاز العمل

  • المحرّر: VS Code (أو ما يعادله) مع ESLint وPrettier.
  • إضافة React Developer Tools في Chrome أو Firefox أو Edge: تضيف التبويبين Components وProfiler إلى أدوات المطوّر (الدرس 3.2).
  • Node.js LTS وnpm محدّثان.

أخطاء متكررة

  • اسم مكوّن بحرف صغير: يُعامَل <carteProduit /> كوسم HTML مجهول. يبدأ المكوّن دائمًا بحرف كبير.
  • فتح index.html مباشرةً في المتصفح (بنقرة مزدوجة): لا تُحوَّل شيفرة JSX، فتبقى الصفحة بيضاء. استعمل npm run dev.
  • وضع vite ضمن dependencies: هذا يعمل، لكنه ليس ممارسة سليمة. أدوات البناء مكانها devDependencies.
  • الاعتقاد بأن React يعوّض JavaScript: كل ما تعلّمته (المصفوفات، الكائنات، map، filter، الوحدات، الوعود (promises)) تحتاجه في كل سطر من React.

التطبيق العملي (Lab)

لا يتوفر الـ lab على شبكة: ستبني يدويًا الهيكل نفسه الذي كان سيولّده npm create vite، لمتجر التعاونية:

  1. الملف package.json بوحدات ES؛
  2. السكربتات dev وbuild وpreview؛
  3. تبعيات React وVite (مصرَّح بها، غير مثبّتة)؛
  4. الصفحة index.html؛
  5. نقطة الدخول main.jsx والمكوّن App.jsx.

اختبار قصير

  1. ما معنى «العرض التصريحي»؟
  2. ماذا يفعل React خلال مرحلة العرض، وخلال مرحلة التثبيت؟
  3. لماذا يُعدّ vite تبعيةً للتطوير؟
  4. ما فائدة العنصر <div id="root"> في index.html؟
  5. متى نفضّل Next.js على تطبيق Vite؟
الأجوبة
  1. نصف الواجهة المنتظرة لحالة معيّنة؛ ويحسب React بنفسه تغييرات الـ DOM ويطبّقها.
  2. العرض: يستدعي المكوّنات ويقارن النتيجة بالعرض السابق. التثبيت: يطبّق على الـ DOM الفروق فقط.
  3. لأنه لا يُستعمل إلا للتطوير ولبناء الملفات؛ والموقع في بيئة الإنتاج لا يحتاج إليه.
  4. إنه الحاوية التي يعرض فيها createRoot(...).render(<App />) التطبيق بأكمله.
  5. لموقع عمومي يحتاج إلى العرض من جهة الخادم (الظهور في محركات البحث، العرض الأول السريع) أو إلى مسارات من جهة الخادم؛ أما Vite فيناسب التطبيقات من جهة العميل التي تُقدَّم على شكل ملفات ثابتة.

المراجع: توثيق React بالفرنسية (fr.react.dev، رخصة CC BY 4.0). نص أصلي من MasterLinux.

تمارين تطبيقية

تُنجَز على Node.js 22 (Debian, git, npm, python3)

الطرفية عبر الإنترنت قادمة قريبًا. في انتظار ذلك، أنجِز هذه المهام على جهازك بنظام لينكس أو على آلة افتراضية أو WSL.

  1. داخل /root/boutique، أنشئ الملف package.json (npm init -y)، ثم سمِّ المشروع boutique-artisanat وصرّح بأنه يستعمل وحدات ES (الحقل "type" بالقيمة "module").
    عرض تلميحcd /root/boutique && npm init -y && npm pkg set name=boutique-artisanat type=module
    عرض الحل
    cd /root/boutique && npm init -y >/dev/null 2>&1 && npm pkg set name=boutique-artisanat type=module

    التحقق : node -e "const p = require('/root/boutique/package.json'); process.exit(p.name === 'boutique-artisanat' && p.type === 'module' ? 0 : 1)"

  2. أضف السكربتات الثلاثة الخاصة بمشروع Vite: dev الذي يشغّل vite، وbuild الذي يشغّل vite build، وpreview الذي يشغّل vite preview.
    عرض تلميحnpm pkg set scripts.dev=vite "scripts.build=vite build" "scripts.preview=vite preview"
    عرض الحل
    cd /root/boutique && npm pkg set scripts.dev=vite "scripts.build=vite build" "scripts.preview=vite preview"

    التحقق : node -e "const s = require('/root/boutique/package.json').scripts || {}; process.exit(s.dev === 'vite' && s.build === 'vite build' && s.preview === 'vite preview' ? 0 : 1)"

  3. لا يتوفر الـ lab على شبكة: صرّح، دون تثبيت، بـ react وreact-dom ضمن التبعيات (dependencies)، ثم بـ vite و@vitejs/plugin-react ضمن تبعيات التطوير (devDependencies). يجب أن يبدأ كل إصدار بالرمز ^ (مثلًا ^19.1.0 بالنسبة لـ react).
    عرض تلميحnpm pkg set dependencies.react=^19.1.0 dependencies.react-dom=^19.1.0 devDependencies.vite=^7.0.0 "devDependencies.@vitejs/plugin-react=^5.0.0"
    عرض الحل
    cd /root/boutique && npm pkg set dependencies.react=^19.1.0 dependencies.react-dom=^19.1.0 devDependencies.vite=^7.0.0 "devDependencies.@vitejs/plugin-react=^5.0.0"

    التحقق : node -e "const p = require('/root/boutique/package.json'); const d = p.dependencies || {}; const dd = p.devDependencies || {}; const v = (x) => /^\^\d/.test(x || ''); process.exit(v(d.react) && v(d['react-dom']) && v(dd.vite) && v(dd['@vitejs/plugin-react']) && !d.vite ? 0 : 1)"

  4. اكتب /root/boutique/index.html: صفحة HTML بالفرنسية (lang="fr") تحتوي على حاوية <div id="root"></div> وتحمّل الوحدة /src/main.jsx بواسطة <script type="module" src="/src/main.jsx"></script>.
    عرض تلميحcat > /root/boutique/index.html <<EOF ... EOF، مع <html lang="fr"> و<body>، ثم الـ div ذي المعرّف root، ثم وسم script
    عرض الحل
    printf '%s\n' '<!doctype html>' '<html lang="fr">' '  <head>' '    <meta charset="UTF-8" />' '    <title>Coopérative Tifawin</title>' '  </head>' '  <body>' '    <div id="root"></div>' '    <script type="module" src="/src/main.jsx"></script>' '  </body>' '</html>' > /root/boutique/index.html

    التحقق : f=/root/boutique/index.html; grep -q 'lang="fr"' $f && grep -q '<div id="root"></div>' $f && grep -q '<script type="module" src="/src/main.jsx"></script>' $f

  5. أنشئ نقطة الدخول /root/boutique/src/main.jsx: تستورد createRoot من "react-dom/client" والمكوّن App من "./App.jsx"، ثم تعرض <App /> داخل العنصر ذي المعرّف root. أنشئ أيضًا /root/boutique/src/App.jsx الذي يصدّر افتراضيًا دالةً App تُرجع عنوانًا <h1>.
    عرض تلميحملفان: main.jsx (import، ثم createRoot(...).render(<App />)) وApp.jsx (export default function App() { return <h1>…</h1>; })
    عرض الحل
    mkdir -p /root/boutique/src && printf '%s\n' 'import { StrictMode } from "react";' 'import { createRoot } from "react-dom/client";' 'import App from "./App.jsx";' '' 'createRoot(document.getElementById("root")).render(' '  <StrictMode>' '    <App />' '  </StrictMode>' ');' > /root/boutique/src/main.jsx && printf '%s\n' 'export default function App() {' '  return <h1>Coopérative Tifawin</h1>;' '}' > /root/boutique/src/App.jsx

    التحقق : m=/root/boutique/src/main.jsx; a=/root/boutique/src/App.jsx; grep -Eq 'import \{ *createRoot *\} from .react-dom/client.' $m && grep -Eq 'import App from .\./App(\.jsx)?.' $m && grep -Eq 'createRoot\(document\.getElementById\(.root.\)\)' $m && grep -Eq '<App */>' $m && grep -Eq 'export default function App\(\)' $a && grep -q '<h1>' $a

المراجع الرسمية المعتمدة: Démarrage rapide — https://fr.react.dev/learn ; Installation — https://fr.react.dev/learn/installation ; Ajouter React à un projet existant — https://fr.react.dev/learn/add-react-to-an-existing-project ; Rendu et Commit — https://fr.react.dev/learn/render-and-commit. نص أصلي من MasterLinux.