دليلك العملي لربط React بـ Node.js و MongoDB وتقفيل الـ MERN Stack

في المقال اللي فات بنينا REST API كامل بعمليات CRUD يربط Express بـ MongoDB. دلوقتي جه دور آخر حرف في MERN: React. هنبني واجهة حقيقية تتصل بالـ API اللي عملناه، تعرض البيانات، وتضيف وتعدل وتحذف منها فعليًا، بدل ما تفضل بيانات وهمية مكتوبة جوه الكود.

المحتويات

أولاً: إعداد مشروع React وربطه بالـ API

مش هعيد شرح إزاي تعمل مشروع React من الصفر، بس المهم إنك تعرف عنوان السيرفر اللي بنيناه في المقال اللي فات، وده هيكون http://localhost:3000 وقت التطوير. هنحتفظ بيه في متغير ثابت فوق الملف عشان نقدر نغيره بسهولة بعدين.

import { useState, useEffect } from "react";

const API_URL = "http://localhost:3000";

function App() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetchUsers();
  }, []);

  async function fetchUsers() {
    const res = await fetch(`${API_URL}/users`);
    const data = await res.json();
    setUsers(data.users);
  }

  return null;
}

export default App;

لاحظ إننا بنستخدم useEffect بمصفوفة تبعية فاضية []، وده معناه إن fetchUsers هتتنفذ مرة واحدة بس أول ما الصفحة تفتح، مش في كل مرة الواجهة تعيد الرسم. لو مش متعود على Hooks زي useState وuseEffect، دي أساسيات React الحديث وتستحق مراجعة سريعة قبل ما تكمل.

ثانياً: جلب وعرض البيانات (Read)

دلوقتي عندنا بيانات المستخدمين جوه state، باقي نعرضها في الصفحة.

return (
  <div>
    <h1>المستخدمين</h1>
    <ul>
      {users.map((user) => (
        <li key={user._id}>
          {user.name} - {user.email}
        </li>
      ))}
    </ul>
  </div>
);

الـ key هنا مهم جدًا، React بيستخدمه عشان يعرف يفرق بين عناصر القايمة لما تتغير، ومعرف المستند _id اللي جاي من MongoDB مثالي للغرض ده لأنه فريد لكل مستخدم.

ثالثاً: فورم لإضافة مستخدم جديد (Create)

هنضيف فورم بسيط بحقلين، وبيبعت طلب POST لنفس route الإنشاء اللي عملناه في المقال اللي فات.

const [name, setName] = useState("");
const [email, setEmail] = useState("");

async function addUser(e) {
  e.preventDefault();
  const res = await fetch(`${API_URL}/users`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ name, email }),
  });
  if (res.ok) {
    setName("");
    setEmail("");
    fetchUsers();
  }
}

return (
  <form onSubmit={addUser}>
    <input value={name} onChange={(e) => setName(e.target.value)} placeholder="الاسم" />
    <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="الإيميل" />
    <button type="submit">إضافة</button>
  </form>
);

لاحظ استخدام e.preventDefault()، ده بيمنع الفورم من إعادة تحميل الصفحة بالطريقة التقليدية، عشان نتحكم في الطلب بأيدينا. وبعد ما الإضافة تنجح بنمسح الحقول ونعيد جلب البيانات (fetchUsers) عشان المستخدم الجديد يظهر في القايمة على طول.

رابعاً: التعديل والحذف من الواجهة

نفس فكرة الإضافة، بس بـ PUT وDELETE على مسار المستخدم بمعرفه.

async function deleteUser(id) {
  await fetch(`${API_URL}/users/${id}`, { method: "DELETE" });
  fetchUsers();
}

async function updateUser(id, newName) {
  await fetch(`${API_URL}/users/${id}`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ name: newName }),
  });
  fetchUsers();
}

الفكرة إن الواجهة نفسها معندهاش أي منطق تعديل مباشر على البيانات، هي بس بتبعت الطلب للسيرفر وبتستدعي fetchUsers تاني عشان تجيب النسخة المحدثة. ده الفرق الأساسي بين تطبيق بيانات وهمية جوه الكود وتطبيق حقيقي مرتبط بقاعدة بيانات: مصدر الحقيقة الوحيد هو السيرفر، مش الواجهة.

الدوال لوحدها مش كفاية، لازم تربطها بزراير فعلية جوه القايمة اللي عملناها في القسم اللي فات.

return (
  <ul>
    {users.map((user) => (
      <li key={user._id}>
        {user.name} - {user.email}
        <button onClick={() => updateUser(user._id, "اسم جديد")}>
          تعديل
        </button>
        <button onClick={() => deleteUser(user._id)}>
          حذف
        </button>
      </li>
    ))}
  </ul>
);

لاحظ إننا بنستخدم arrow function جوه onClick (() => deleteUser(user._id)) بدل ما نكتب onClick={deleteUser(user._id)} مباشرة. الفرق مهم: الشكل التاني هينفذ الدالة فورًا لحظة ما الصفحة ترسم، مش لما المستخدم يدوس الزرار فعليًا. الشكل الأول بس هو اللي بيأجل التنفيذ لحد لحظة الضغط الحقيقية. في مشروع حقيقي، غالبًا هتستخدم فورم تعديل منفصل بدل النص الثابت “اسم جديد”، بس المبدأ هيفضل نفسه.

خامساً: حالات التحميل والأخطاء

لحد دلوقتي لو النت بطيء أو السيرفر واقع، المستخدم هيشوف صفحة فاضية من غير أي تفسير. هنضيف حالتين: loading وerror.

const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

async function fetchUsers() {
  try {
    setLoading(true);
    const res = await fetch(`${API_URL}/users`);
    if (!res.ok) throw new Error("فشل تحميل البيانات");
    const data = await res.json();
    setUsers(data.users);
    setError(null);
  } catch (err) {
    setError(err.message);
  } finally {
    setLoading(false);
  }
}

if (loading) return <p>بيتم التحميل...</p>;
if (error) return <p>حصل خطأ: {error}</p>;

الـ finally هنا بيتنفذ في كل الحالات، نجح الطلب أو فشل، فهو المكان المناسب لإيقاف حالة loading. لو نسيتها، الصفحة ممكن تفضل عالقة على “بيتم التحميل…” حتى لو الطلب فشل بالفعل.

سادساً: أخطاء شائعة (زي CORS)

أشهر مشكلة هتقابلك وانت بتربط React بسيرفر Express شغال على بورت مختلف هي رسالة خطأ متعلقة بـ CORS (Cross-Origin Resource Sharing) في Console المتصفح. المتصفح بيرفض الطلب لأن السيرفر مصرحلوش صراحة يستقبل طلبات من عنوان تطبيق الـ React بتاعك.

الحل بسيط، رجع لملف السيرفر وضيف مكتبة cors.

const cors = require("cors");
app.use(cors());

السطر ده بيسمح لأي عنوان يوصل السيرفر، وده كويس وقت التطوير بس. أخطاء تانية شائعة: نسيان تشغيل السيرفر أصلاً قبل تجربة الواجهة، أو كتابة عنوان الـ API_URL غلط (زي فرق بورت 3000 وبورت 3001)، أو محاولة قراءة data.users من استجابة السيرفر مش شكلها كده أصلاً.

سابعاً: خطوة قبل النشر

قبل ما تنشر التطبيق فعليًا، فيه نقطتين مهمين. الأولى: API_URL اللي كاتبه ثابت (http://localhost:3000) لازم يتغير لعنوان السيرفر الحقيقي بعد النشر، والأفضل تحطه في متغير بيئة (environment variable) بدل ما يفضل مكتوب صريح جوه الكود، عشان تقدر تغيره بسهولة بين بيئة التطوير والإنتاج من غير تعديل في الكود نفسه.

الثانية: افتح cors() اللي عملناه في القسم اللي فات محدد بس للعنوان الحقيقي بتاع تطبيقك، مش مفتوح لأي عنوان، وده امتداد لنفس منطق الحماية اللي اتكلمنا عنه في مقالي MongoDB Atlas والـ API: افتح بس اللي محتاجه، وسكّر الباقي.

دلوقتي عندك تطبيق MERN متكامل: واجهة React بتتكلم مع سيرفر Express، والسيرفر بيتكلم مع قاعدة بيانات MongoDB حقيقية. لو وصلت للنقطة دي، تكون خلصت أول مشروع Full Stack كامل بإيدك، من الصفر لحد النشر. لو عندك سؤال أو علقت في خطوة معينة في السلسلة، قولّي في التعليقات.

ثامناً: تقسيم الكود لـ Components منفصلة

كل الكود اللي كتبناه لحد دلوقتي جوه مكون واحد اسمه App، وده مقبول لمشروع تعليمي صغير، بس لو المشروع كبر شوية هتلاقي نفس مشكلة التكرار اللي اتكلمنا عنها في الكود الخلفي (نفس فكرة الـ Clean Code اللي شرحتها في مقال أهمية الـ Clean Code بتنطبق على الواجهة برضو، مش الكود الخلفي بس).

الحل المعتاد في React هو تقسيم الشاشة لمكونات صغيرة، كل واحد له مسؤولية واحدة واضحة:

  • UserForm: مسؤول بس عن فورم الإضافة والحقول والتحقق منها.
  • UserList: مسؤول بس عن عرض القايمة.
  • UserItem: مسؤول عن صف واحد بس (اسم، إيميل، زراير تعديل وحذف).
  • App: مسؤول بس عن تجميع المكونات دي مع بعض وتمرير البيانات والدوال بينهم عن طريق props.

الفايدة العملية: لو حصل خطأ في عرض صف مستخدم واحد، تعرف بالظبط تدور فين (جوه UserItem)، مش تفتش في ملف App كامل. ولو عايز تعيد استخدام الفورم في صفحة تانية بعدين، تقدر تنقله بسهولة لأنه مكون مستقل بذاته. التقسيم ده مش تعقيد زيادة، هو بالعكس أسهل طريقة تحافظ بيها على الكود مفهوم كل ما المشروع يكبر.


اكتشاف المزيد من كود التطور

اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.

اترك رد

Scroll to Top