Next.js
جلب البيانات في Next.js — دليل شامل 2026
📅 2026-11-15⏱ 9 دقائق قراءة
في المقال السابق، تعلمت Server Components. الآن سنتعمق في **جلب البيانات (Data Fetching)** — وهو جوهر أي تطبيق ويب حقيقي.
في هذا الدليل العملي، سنأخذك خطوة بخطوة لفهم جلب البيانات في Next.js، مع تمارين وحلول.
## طرق جلب البيانات
<table>
<thead>
<tr>
<th>الطريقة</th>
<th>المكان</th>
<th>الاستخدام</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>fetch في Server Component</strong></td>
<td>السيرفر</td>
<td>الأكثر شيوعاً</td>
</tr>
<tr>
<td><strong>Server Actions</strong></td>
<td>السيرفر</td>
<td>البيانات المتغيرة (POST/PUT)</td>
</tr>
<tr>
<td><strong>Route Handlers</strong></td>
<td>السيرفر</td>
<td>API Endpoints</td>
</tr>
<tr>
<td><strong>useEffect</strong></td>
<td>المتصفح</td>
<td>البيانات الديناميكية</td>
</tr>
<tr>
<td><strong>SWR / React Query</strong></td>
<td>المتصفح</td>
<td>التخزين المؤقت المتقدم</td>
</tr>
</tbody>
</table>
## fetch في Server Component
**أسهل طريقة:** استخدام `fetch` مباشرة في Server Component.
```tsx
async function getUsers() {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
return res.json();
}
export default async function UsersPage() {
const users = await getUsers();
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
```
**الفائدة:**
- ✅ يعمل على السيرفر.
- ✅ SEO ممتاز.
- ✅ Bundle أصغر.
- ✅ مباشر — لا `useEffect`.
## cache في fetch
Next.js يوسّع `fetch` بـ `cache` و `next.revalidate`.
### 1. التخزين الدائم (Static)
```tsx
fetch(url, { cache: "force-cache" }); // ← افتراضي
```
**السلوك:** تُجلب البيانات مرة واحدة، وتُخزّن للأبد.
**الاستخدام:** بيانات لا تتغير (مقالات، صفحات ثابتة).
### 2. لا تخزين (Dynamic)
```tsx
fetch(url, { cache: "no-store" });
```
**السلوك:** تُجلب البيانات **في كل طلب**.
**الاستخدام:** بيانات حية (أسعار، أخبار).
### 3. إعادة التحقق (ISR)
```tsx
fetch(url, { next: { revalidate: 60 } }); // ← كل 60 ثانية
```
**السلوك:** تُجلب البيانات، وتُخزّن، وتُحدَّث كل 60 ثانية.
**الاستخدام:** بيانات تتغير بشكل دوري.
<table>
<thead>
<tr>
<th>الإعداد</th>
<th>السلوك</th>
<th>الاستخدام</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>force-cache</code></td>
<td>تخزين دائم</td>
<td>بيانات ثابتة</td>
</tr>
<tr>
<td><code>no-store</code></td>
<td>لا تخزين</td>
<td>بيانات حية</td>
</tr>
<tr>
<td><code>revalidate: 60</code></td>
<td>كل 60 ثانية</td>
<td>بيانات دورية</td>
</tr>
</tbody>
</table>
## Parallel Data Fetching
**لجلب عدة مصادر بيانات بالتوازي:**
```tsx
async function getData() {
const [users, posts, comments] = await Promise.all([
fetch("https://api.example.com/users").then((r) => r.json()),
fetch("https://api.example.com/posts").then((r) => r.json()),
fetch("https://api.example.com/comments").then((r) => r.json()),
]);
return { users, posts, comments };
}
export default async function Page() {
const { users, posts, comments } = await getData();
return (
<div>
<p>المستخدمون: {users.length}</p>
<p>المقالات: {posts.length}</p>
<p>التعليقات: {comments.length}</p>
</div>
);
}
```
**الفائدة:** **أسرع 3 مرات** من التسلسل.
## Sequential Data Fetching
**إذا كانت البيانات تعتمد على بعضها:**
```tsx
async function getUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`);
return res.json();
}
async function getPosts(userId: string) {
const res = await fetch(`https://api.example.com/users/${userId}/posts`);
return res.json();
}
export default async function UserPage() {
const user = await getUser("1"); // ← أولاً
const posts = await getPosts(user.id); // ← بعدها
return (
<div>
<h1>{user.name}</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
```
## Suspense و Streaming
**Suspense** تسمح بتحميل الصفحة **بالتدريج**.
```tsx
import { Suspense } from "react";
async function SlowComponent() {
const data = await fetch("https://api.example.com/slow").then((r) => r.json());
return <p>{data.value}</p>;
}
async function FastComponent() {
const data = await fetch("https://api.example.com/fast").then((r) => r.json());
return <p>{data.value}</p>;
}
export default function Dashboard() {
return (
<div>
<h1>لوحة التحكم</h1>
<Suspense fallback={<p>جاري تحميل السريع...</p>}>
<FastComponent />
</Suspense>
<Suspense fallback={<p>جاري تحميل البطيء...</p>}>
<SlowComponent />
</Suspense>
</div>
);
}
```
**النتيجة:** الجزء السريع يظهر أولاً، والبطيء لاحقاً.
## loading.tsx
**لصفحة كاملة:**
```tsx
// app/users/loading.tsx
export default function Loading() {
return (
<div className="flex items-center justify-center p-12">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-blue-600 border-t-transparent" />
</div>
);
}
```
**النتيجة:** تظهر شاشة تحميل أثناء جلب البيانات.
## error.tsx
**لمعالجة الأخطاء:**
```tsx
// app/users/error.tsx
"use client";
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div className="text-center p-12">
<h1 className="text-2xl font-bold text-red-600 mb-4">
❌ حدث خطأ
</h1>
<p className="text-gray-600 mb-6">{error.message}</p>
<button
onClick={reset}
className="bg-blue-600 text-white px-6 py-3 rounded-lg"
>
حاول مرة أخرى
</button>
</div>
);
}
```
## Server Actions (البيانات المتغيرة)
**Server Actions** هي دوال تعمل على **السيرفر**، وتُستدعى من العميل.
### 1. تعريف Server Action
**`app/actions.ts`:**
```tsx
"use server";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const content = formData.get("content") as string;
// ... حفظ في قاعدة البيانات
console.log("عنوان:", title);
console.log("محتوى:", content);
// إعادة التحقق من المسار
// revalidatePath("/posts");
}
```
### 2. استخدامها في Client
**`app/posts/new/page.tsx`:**
```tsx
import { createPost } from "@/app/actions";
export default function NewPostPage() {
return (
<form action={createPost} className="max-w-lg mx-auto p-8">
<h1 className="text-2xl font-bold mb-6">مقال جديد</h1>
<input
name="title"
placeholder="العنوان"
required
className="w-full px-4 py-3 border rounded-lg mb-4"
/>
<textarea
name="content"
placeholder="المحتوى"
required
rows={6}
className="w-full px-4 py-3 border rounded-lg mb-4"
/>
<button
type="submit"
className="bg-blue-600 text-white px-6 py-3 rounded-lg"
>
نشر
</button>
</form>
);
}
```
**الفائدة:** لا تحتاج API Route!
## مثال عملي: مدونة مع Server Actions
### 1. Actions `app/posts/actions.ts`
```tsx
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
let posts = [
{ id: 1, title: "مقال 1", content: "..." },
];
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const content = formData.get("content") as string;
const newPost = {
id: Date.now(),
title,
content,
};
posts.push(newPost);
revalidatePath("/posts");
redirect("/posts");
}
export async function deletePost(id: number) {
posts = posts.filter((p) => p.id !== id);
revalidatePath("/posts");
}
export async function getPosts() {
return posts;
}
```
### 2. الصفحة `app/posts/page.tsx`
```tsx
import Link from "next/link";
import { getPosts } from "./actions";
export default async function PostsPage() {
const posts = await getPosts();
return (
<div className="max-w-4xl mx-auto p-8">
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold">المقالات</h1>
<Link
href="/posts/new"
className="bg-blue-600 text-white px-6 py-3 rounded-lg"
>
+ مقال جديد
</Link>
</div>
<ul className="space-y-4">
{posts.map((post) => (
<li key={post.id} className="bg-white p-6 rounded-xl shadow-sm">
<h2 className="text-xl font-bold mb-2">{post.title}</h2>
<p className="text-gray-600">{post.content}</p>
</li>
))}
</ul>
</div>
);
}
```
### 3. صفحة جديدة `app/posts/new/page.tsx`
```tsx
import { createPost } from "../actions";
export default function NewPostPage() {
return (
<form action={createPost} className="max-w-lg mx-auto p-8">
<h1 className="text-2xl font-bold mb-6">مقال جديد</h1>
<input
name="title"
placeholder="العنوان"
required
className="w-full px-4 py-3 border rounded-lg mb-4"
/>
<textarea
name="content"
placeholder="المحتوى"
required
rows={6}
className="w-full px-4 py-3 border rounded-lg mb-4"
/>
<button
type="submit"
className="bg-blue-600 text-white px-6 py-3 rounded-lg"
>
نشر
</button>
</form>
);
}
```
## useEffect في Client
**للبحث المباشر، أو البيانات التي تتغير بناءً على تفاعل المستخدم:**
```tsx
"use client";
import { useState, useEffect } from "react";
export default function SearchPage() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!query) {
setResults([]);
return;
}
setLoading(true);
const timer = setTimeout(async () => {
const res = await fetch(`/api/search?q=${query}`);
const data = await res.json();
setResults(data);
setLoading(false);
}, 500);
return () => clearTimeout(timer);
}, [query]);
return (
<div className="max-w-2xl mx-auto p-8">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="ابحث..."
className="w-full px-4 py-3 border rounded-lg mb-4"
/>
{loading && <p>جاري البحث...</p>}
<ul>
{results.map((r) => (
<li key={r.id}>{r.title}</li>
))}
</ul>
</div>
);
}
```
## جدول أوقات جلب البيانات
<table>
<thead>
<tr>
<th>الأسلوب</th>
<th>المكان</th>
<th>التوقيت</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Server + fetch</strong></td>
<td>السيرفر</td>
<td>قبل العرض</td>
</tr>
<tr>
<td><strong>Server Actions</strong></td>
<td>السيرفر</td>
<td>عند إرسال النموذج</td>
</tr>
<tr>
<td><strong>useEffect</strong></td>
<td>المتصفح</td>
<td>بعد العرض</td>
</tr>
<tr>
<td><strong>SWR</strong></td>
<td>المتصفح</td>
<td>بعد العرض (مع cache)</td>
</tr>
</tbody>
</table>
## تمارين عملية
### تمرين 1: جلب مستخدمين
اجلب قائمة مستخدمين من API.
**الحل:**
```tsx
async function getUsers() {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
return res.json();
}
export default async function UsersPage() {
const users = await getUsers();
return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}
```
### تمرين 2: cache: no-store
اجلب بيانات لا تُخزّن.
**الحل:**
```tsx
const res = await fetch(url, { cache: "no-store" });
```
### تمرين 3: ISR
اجلب بيانات مع إعادة تحقق كل 60 ثانية.
**الحل:**
```tsx
const res = await fetch(url, { next: { revalidate: 60 } });
```
### تمرين 4: Parallel Fetching
اجلب مصدرين بالتوازي.
**الحل:**
```tsx
const [users, posts] = await Promise.all([
fetch("...").then((r) => r.json()),
fetch("...").then((r) => r.json()),
]);
```
### تمرين 5: Suspense
أضف Suspense لمكون بطيء.
**الحل:**
```tsx
<Suspense fallback={<p>جاري التحميل...</p>}>
<SlowComponent />
</Suspense>
```
### تمرين 6: Server Action
أنشئ Server Action لإضافة عنصر.
**الحل:**
```tsx
"use server";
export async function addItem(formData: FormData) {
const name = formData.get("name") as string;
// ... حفظ
revalidatePath("/items");
}
```
### تمرين 7: useEffect
أنشئ بحثاً مع debouncing.
**الحل:** (راجع مثال البحث أعلاه)
### تمرين 8: تطبيق كامل
ابنِ تطبيق CRUD كامل مع Server Actions.
**الحل:** (راجع مثال المدونة أعلاه)
## حل المشاكل الشائعة
### 🔴 المشكلة 1: البيانات لا تُحدّث
**السبب:** cache يخزّن البيانات.
**الحل:**
```tsx
fetch(url, { cache: "no-store" });
```
أو:
```tsx
import { revalidatePath } from "next/cache";
revalidatePath("/posts");
```
### 🔴 المشكلة 2: `fetch` لا يعمل في Client
**السبب:** تحتاج URL كامل.
**الحل:** استخدم URL نسبي أو كامل:
```tsx
fetch("/api/users"); // ← نسبي
fetch("https://api.example.com/users"); // ← كامل
```
### 🔴 المشكلة 3: Suspense لا يعمل
**السبب:** المكون ليس `async`.
**الحل:** تأكد من أن المكون `async`.
### 🔴 المشكلة 4: Server Action لا يعمل
**السبب:** لم تُضف `"use server"`.
**الحل:** أضف في أعلى الملف.
### 🔴 المشكلة 5: `revalidatePath is not a function`
**الحل:**
```tsx
import { revalidatePath } from "next/cache";
```
## جدول أدوات البيانات
<table>
<thead>
<tr>
<th>الأداة</th>
<th>الاستخدام</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>fetch</code></td>
<td>جلب البيانات</td>
</tr>
<tr>
<td><code>cache: "no-store"</code></td>
<td>لا تخزين</td>
</tr>
<tr>
<td><code>revalidate: 60</code></td>
<td>ISR</td>
</tr>
<tr>
<td><code>Promise.all</code></td>
<td>Parallel</td>
</tr>
<tr>
<td><code>Suspense</code></td>
<td>Streaming</td>
</tr>
<tr>
<td><code>Server Actions</code></td>
<td>البيانات المتغيرة</td>
</tr>
<tr>
<td><code>revalidatePath</code></td>
<td>إعادة التحقق</td>
</tr>
</tbody>
</table>
## قائمة تحقق نهائية
<table>
<thead>
<tr>
<th>المهمة</th>
<th>الحالة</th>
</tr>
</thead>
<tbody>
<tr>
<td>fetch في Server Component</td>
<td>⬜</td>
</tr>
<tr>
<td>cache: no-store</td>
<td>⬜</td>
</tr>
<tr>
<td>ISR (revalidate)</td>
<td>⬜</td>
</tr>
<tr>
<td>Parallel Fetching</td>
<td>⬜</td>
</tr>
<tr>
<td>Suspense</td>
<td>⬜</td>
</tr>
<tr>
<td>Server Actions</td>
<td>⬜</td>
</tr>
<tr>
<td>حل التمارين الثمانية</td>
<td>⬜</td>
</tr>
</tbody>
</table>
## ماذا بعد هذا المقال؟
الآن بعد أن أتقنت جلب البيانات، أنت جاهز للمقال التالي:
1. **API Routes** — بناء APIs.
2. **Styling** — Tailwind و CSS.
3. **Authentication** — المصادقة.
## الخلاصة
في هذا المقال، تعلمت:
- ✅ fetch في Server Component.
- ✅ cache (force, no-store, revalidate).
- ✅ Parallel vs Sequential.
- ✅ Suspense و Streaming.
- ✅ loading.tsx و error.tsx.
- ✅ Server Actions.
- ✅ useEffect في Client.
- ✅ حل 8 تمارين عملية.
**تذكر:** جلب البيانات هو **قلب** أي تطبيق. أتقنه جيداً.