طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 242 · أطر الويب
الدرس 18 · الوحدة 7: تصميم واجهات برمجة التطبيقات RESTful

ربط React بالـ API

تطبيق كامل: واجهة تطلب، وخادم يرد ⏱ نحو 50 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تربط واجهة React بخادم Express عبر fetch.
  • تعرض حالات التحميل والنجاح والخطأ وتفحص res.ok.
  • ترسل بيانات JSON بـ POST وتحدّث الحالة من رد الخادم.
  • تنظّم الطلبات في طبقة api، وترسل الرمز، وتفهم CORS ووكيل Vite.

١طرفان يتحادثان

أصبح لدينا نصفا التطبيق: واجهة React (يشغّلها Vite على المنفذ 5173) وخادم Express (على المنفذ 3000). تتحادثان عبر HTTP: الواجهة ترسل fetch، والخادم يرد بـ JSON، والواجهة تحدّث الحالة فيُعاد الرسم.

الخطوةأينالكود
1. المكوّن يظهرReactuseEffect(() => {…}, [])
2. طلب HTTPالمتصفحfetch('http://localhost:3000/api/books')
3. المعالجة والردExpressres.json(books)
4. قراءة الردReactawait res.json()
5. تحديث الحالةReactsetBooks(data) ← إعادة رسم

٢القراءة: GET مع التحميل والخطأ

كل طلب شبكة له ثلاث حالات يجب أن تعرضها الواجهة: جارٍ التحميل، ونجح، وفشل. وانتبه: fetch لا يرمي خطأً عند 404 أو 500؛ يرمي فقط إن تعذّر الاتصال. لذلك نفحص res.ok بأنفسنا:

import { useState, useEffect } from 'react'; const API = 'http://localhost:3000'; export default function App() {  const [books, setBooks] = useState([]);  const [loading, setLoading] = useState(true);  const [error, setError] = useState(null);   useEffect(() => {    fetch(`${API}/api/books`)      .then(res => {        if (!res.ok) throw new Error('فشل الطلب: ' + res.status);        return res.json();      })      .then(data => setBooks(data))      .catch(err => setError(err.message))      .finally(() => setLoading(false));  }, []);   if (loading) return <p>⏳ جارٍ التحميل…</p>;  if (error) return <p className="err">⚠ {error}</p>;  return (    <ul>      {books.map(b => <li key={b.id}>{b.title} — {b.author}</li>)}    </ul>  );}

٣الكتابة: POST من نموذج

نرسل البيانات بـ method: 'POST' والترويسة Content-Type: application/json والجسم JSON.stringify(…). وبعد النجاح نضيف إلى القائمة ما أعاده الخادم (ومعه id الذي ولّده)، لا ما كتبه المستخدم. اترك الحقلين فارغين واضغط «أضف» لترى رسالة الخادم:

import { useState, useEffect } from 'react'; const API = 'http://localhost:3000'; export default function App() {  const [books, setBooks] = useState([]);  const [title, setTitle] = useState('');  const [author, setAuthor] = useState('');  const [error, setError] = useState('');  const [saving, setSaving] = useState(false);   useEffect(() => {    fetch(`${API}/api/books`).then(r => r.json()).then(setBooks);  }, []);   async function add(e) {    e.preventDefault();    setSaving(true); setError('');    const res = await fetch(`${API}/api/books`, {      method: 'POST',      headers: { 'Content-Type': 'application/json' },      body: JSON.stringify({ title, author }),    });    const data = await res.json();    setSaving(false);    if (!res.ok) return setError(data.error);       // رسالة الخادم نفسها    setBooks([...books, data]);                       // أضف ما أعاده الخادم    setTitle(''); setAuthor('');  }   return (    <div>      <form onSubmit={add}>        <input value={title} onChange={e => setTitle(e.target.value)} placeholder="العنوان" />        <input value={author} onChange={e => setAuthor(e.target.value)} placeholder="المؤلف" />        <button disabled={saving}>{saving ? '…' : 'أضف'}</button>      </form>      {error && <p className="err">⚠ {error}</p>}      <ul>{books.map(b => <li key={b.id}>{b.title} — {b.author}</li>)}</ul>    </div>  );}

٤طبقة api منفصلة

تكرار fetch وفحص res.ok وتحويل JSON في كل مكوّن يملأ الكود بالتفاصيل. نجمعها في ملف api.js واحد، فتبقى المكوّنات بسيطة، ويسهل لاحقًا إضافة الرمز لكل الطلبات أو تغيير عنوان الخادم:

import { useState, useEffect } from 'react';import { api } from './api.js'; export default function App() {  const [books, setBooks] = useState([]);  const [msg, setMsg] = useState('');   useEffect(() => { api.get('/api/books').then(setBooks); }, []);   async function quickAdd() {    try {      const b = await api.post('/api/books', { title: 'كتاب سريع', author: 'المكتبة' });      setBooks(prev => [...prev, b]);      setMsg('أُضيف #' + b.id);    } catch (err) {      setMsg('⚠ ' + err.message);    }  }   return (    <div>      <button onClick={quickAdd}>إضافة سريعة</button> <span className="msg">{msg}</span>      <p>عدد الكتب: <b>{books.length}</b></p>    </div>  );}

٥الواجهة والمصادقة

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

import { useState } from 'react'; const API = 'http://localhost:3000'; export default function App() {  const [token, setToken] = useState(null);  const [secret, setSecret] = useState('');  const [pass, setPass] = useState('');  const [err, setErr] = useState('');   async function login(e) {    e.preventDefault();    const res = await fetch(`${API}/api/login`, {      method: 'POST', headers: { 'Content-Type': 'application/json' },      body: JSON.stringify({ password: pass }),    });    const data = await res.json();    if (!res.ok) return setErr(data.error);    setToken(data.token); setErr('');  }   async function load() {    const res = await fetch(`${API}/api/secret`, { headers: { Authorization: `Bearer ${token}` } });    setSecret((await res.json()).msg);  }   if (!token) return (    <form onSubmit={login}>      <input type="password" value={pass} onChange={e => setPass(e.target.value)} placeholder="كلمة المرور" />      <button>دخول</button>      {err && <p className="err">{err}</p>}    </form>  );  return (    <div>      <p>✓ مسجّل الدخول</p>      <button onClick={load}>اعرض المحتوى المحمي</button> <b className="secret">{secret}</b>      <button onClick={() => setToken(null)}>خروج</button>    </div>  );}

٦CORS ووكيل Vite

الواجهة على localhost:5173 والخادم على localhost:3000: منفذان مختلفان = أصلان Origins مختلفان. المتصفح يمنع افتراضيًّا أن تقرأ صفحةٌ ردًّا من أصل آخر، إلا إن سمح الخادم بالترويسة Access-Control-Allow-Origin. هذا ما يضيفه وسيط cors. وبدونه تظهر في Console رسالة «blocked by CORS policy».

حل آخر شائع أثناء التطوير: وكيل Proxy في Vite يمرر طلبات /api إلى الخادم، فتبدو للمتصفح من الأصل نفسه، ونكتب في الواجهة fetch('/api/books') بلا عنوان:

// vite.config.jsimport { defineConfig } from 'vite';import react from '@vitejs/plugin-react'; export default defineConfig({  plugins: [react()],  server: {    proxy: { '/api': 'http://localhost:3000' },  },});

توقّع وتحقّق

متى يرمي fetch خطأً (يدخل catch)؟

res.ok للباقي.

ما الذي يجعل req.body يصل كائنًا عند إرسال JSON من React؟

الطرفان.

لماذا يظهر خطأ CORS بين 5173 و 3000؟

Same-origin policy.

أين نضع الرمز في طلبات الواجهة؟

Bearer.

الخلاصة

تحدٍّ برمجي: قائمة مهام متصلة بالخادم

متقدم

الخادم جاهز في server/server.js. اكتب واجهة React تتعامل معه.

المطلوب:
  • عند الظهور: اجلب GET /api/todos واعرض كل مهمة في <li> داخل <ul className="todos">.
  • نموذج فيه <input> وزر؛ عند الإرسال: POST /api/todos بالجسم {"text": …}، ثم أضف المهمة التي أعادها الخادم وأفرغ الحقل.
  • إن رفض الخادم (نص فارغ) اعرض رسالته في <p className="err">.
👀 اعرض الناتج المطلوب (وجرّبه)
✓ التحقق الآلي من حلّك:
  • الكود يعمل بلا أخطاء في Console
  • تعرض المهمتين القادمتين من الخادم
  • إضافة مهمة ترسلها للخادم وتظهر
  • الحقل يُفرغ بعد الإضافة
  • النص الفارغ يعرض رسالة الخادم
  • يستخدم fetch مع POST و JSON.stringify

💡 تلميح 1
الجلب في useEffect(…, [])، والإضافة في دالة async مربوطة بـ onSubmit مع e.preventDefault().
💡 تلميح 2
اقرأ JSON الرد أولًا، ثم افحص res.ok: إن كان false فالرسالة في data.error.

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

import { useState, useEffect } from 'react'; const API = 'http://localhost:3000'; export default function App() {  const [todos, setTodos] = useState([]);  const [text, setText] = useState('');  const [error, setError] = useState('');   useEffect(() => {    fetch(`${API}/api/todos`).then(r => r.json()).then(setTodos);  }, []);   async function add(e) {    e.preventDefault();    const res = await fetch(`${API}/api/todos`, {      method: 'POST',      headers: { 'Content-Type': 'application/json' },      body: JSON.stringify({ text }),    });    const data = await res.json();    if (!res.ok) return setError(data.error);    setTodos([...todos, data]);    setText(''); setError('');  }   return (    <div>      <form onSubmit={add}>        <input value={text} onChange={e => setText(e.target.value)} />        <button>أضف</button>      </form>      {error && <p className="err">{error}</p>}      <ul className="todos">{todos.map(t => <li key={t.id}>{t.text}</li>)}</ul>    </div>  );}
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس