ربط React بالـ API
تطبيق كامل: واجهة تطلب، وخادم يرد ⏱ نحو 50 دقيقة.
- تربط واجهة
ReactبخادمExpressعبرfetch. - تعرض حالات التحميل والنجاح والخطأ وتفحص
res.ok. - ترسل بيانات
JSONبـPOSTوتحدّث الحالة من رد الخادم. - تنظّم الطلبات في طبقة
api، وترسل الرمز، وتفهمCORSووكيلVite.
١طرفان يتحادثان
أصبح لدينا نصفا التطبيق: واجهة React (يشغّلها Vite على المنفذ 5173) وخادم Express (على المنفذ 3000). تتحادثان عبر HTTP: الواجهة ترسل fetch، والخادم يرد بـ JSON، والواجهة تحدّث الحالة فيُعاد الرسم.
| الخطوة | أين | الكود |
|---|---|---|
| 1. المكوّن يظهر | React | useEffect(() => {…}, []) |
| 2. طلب HTTP | المتصفح | fetch('http://localhost:3000/api/books') |
| 3. المعالجة والرد | Express | res.json(books) |
| 4. قراءة الرد | React | await res.json() |
| 5. تحديث الحالة | React | setBooks(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> );}
// الخادم: Express على المنفذ 3000const express = require('express');const cors = require('cors');const app = express();app.use(cors({ origin: 'http://localhost:5173' }));app.use(express.json()); let books = [ { id: 1, title: 'تعلم JavaScript', author: 'سارة' }, { id: 2, title: 'أساسيات الويب', author: 'خالد' },]; app.get('/api/books', (req, res) => res.json(books)); app.post('/api/books', (req, res) => { const { title, author } = req.body; if (!title || !author) return res.status(400).json({ error: 'العنوان والمؤلف مطلوبان' }); const book = { id: books.length + 1, title, author }; books.push(book); res.status(201).json(book);}); app.listen(3000);
٣الكتابة: 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> );}
// الخادم: Express على المنفذ 3000const express = require('express');const cors = require('cors');const app = express();app.use(cors({ origin: 'http://localhost:5173' }));app.use(express.json()); let books = [ { id: 1, title: 'تعلم JavaScript', author: 'سارة' }, { id: 2, title: 'أساسيات الويب', author: 'خالد' },]; app.get('/api/books', (req, res) => res.json(books)); app.post('/api/books', (req, res) => { const { title, author } = req.body; if (!title || !author) return res.status(400).json({ error: 'العنوان والمؤلف مطلوبان' }); const book = { id: books.length + 1, title, author }; books.push(book); res.status(201).json(book);}); app.listen(3000);
٤طبقة 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: كل التواصل مع الخادم في مكان واحدconst BASE = 'http://localhost:3000';let token = null; // يُضبط بعد تسجيل الدخولexport const setToken = (t) => { token = t; }; async function request(method, path, body) { const res = await fetch(BASE + path, { method, headers: { 'Content-Type': 'application/json', ...(token && { Authorization: `Bearer ${token}` }), }, body: body ? JSON.stringify(body) : undefined, }); const data = res.status === 204 ? null : await res.json(); if (!res.ok) throw new Error(data?.error || 'خطأ ' + res.status); return data;} export const api = { get: (p) => request('GET', p), post: (p, b) => request('POST', p, b), patch: (p, b) => request('PATCH', p, b), del: (p) => request('DELETE', p),};
// الخادم: Express على المنفذ 3000const express = require('express');const cors = require('cors');const app = express();app.use(cors({ origin: 'http://localhost:5173' }));app.use(express.json()); let books = [ { id: 1, title: 'تعلم JavaScript', author: 'سارة' }, { id: 2, title: 'أساسيات الويب', author: 'خالد' },]; app.get('/api/books', (req, res) => res.json(books)); app.post('/api/books', (req, res) => { const { title, author } = req.body; if (!title || !author) return res.status(400).json({ error: 'العنوان والمؤلف مطلوبان' }); const book = { id: books.length + 1, title, author }; books.push(book); res.status(201).json(book);}); app.listen(3000);
٥الواجهة والمصادقة
بعد تسجيل الدخول يُحفظ الرمز (هنا في الحالة وفي 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> );}
const express = require('express');const jwt = require('jsonwebtoken');const app = express();app.use(express.json());const SECRET = 'demo-secret-r18'; app.post('/api/login', (req, res) => { if (req.body.password !== '1234') return res.status(401).json({ error: 'كلمة المرور غير صحيحة' }); res.json({ token: jwt.sign({ sub: 1 }, SECRET, { expiresIn: '15m' }) });}); app.get('/api/secret', (req, res) => { try { jwt.verify((req.headers.authorization || '').slice(7), SECRET); res.json({ msg: 'الدرجات النهائية تُنشر الأحد 🎉' }); } catch { res.status(401).json({ msg: 'ممنوع' }); }}); app.listen(3000);
٦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.الخلاصة
- الجلب في
useEffect، والإرسال في معالج الحدث. fetchلا يرمي عند404/500: افحصres.ok.POST = method + Content-Type + JSON.stringify.- طبقة
api.jsتوحّد العنوان والترويسات والأخطاء والرمز. - أصلان مختلفان يحتاجان
corsفي الخادم، أوproxyفيViteأثناء التطوير.
تحدٍّ برمجي: قائمة مهام متصلة بالخادم
متقدمالخادم جاهز في 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
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> );}
const express = require('express');const app = express();app.use(express.json()); const todos = [{ id: 1, text: 'مراجعة الدرس' }, { id: 2, text: 'حل التحدي' }]; app.get('/api/todos', (req, res) => res.json(todos));app.post('/api/todos', (req, res) => { if (!req.body.text || !req.body.text.trim()) return res.status(400).json({ error: 'اكتب نص المهمة' }); const t = { id: todos.length + 1, text: req.body.text.trim() }; todos.push(t); res.status(201).json(t);}); app.listen(3000);