جلب البيانات في Next.js — دليل شامل 2026
دليل عملي مفصل لجلب البيانات في Next.js — fetch، cache، ISR، Server Actions، مع تمارين وحلول
في المقال السابق، تعلمت Server Components. الآن سنتعمق في جلب البيانات (Data Fetching) — وهو جوهر أي تطبيق ويب حقيقي.
في هذا الدليل العملي، سنأخذك خطوة بخطوة لفهم جلب البيانات في Next.js، مع تمارين وحلول.
طرق جلب البيانات
| الطريقة | المكان | الاستخدام |
|---|---|---|
| fetch في Server Component | السيرفر | الأكثر شيوعاً |
| Server Actions | السيرفر | البيانات المتغيرة (POST/PUT) |
| Route Handlers | السيرفر | API Endpoints |
| useEffect | المتصفح | البيانات الديناميكية |
| SWR / React Query | المتصفح | التخزين المؤقت المتقدم |
fetch في Server Component
أسهل طريقة: استخدام fetch مباشرة في Server Component.
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)
fetch(url, { cache: "force-cache" }); // ← افتراضي
السلوك: تُجلب البيانات مرة واحدة، وتُخزّن للأبد.
الاستخدام: بيانات لا تتغير (مقالات، صفحات ثابتة).
2. لا تخزين (Dynamic)
fetch(url, { cache: "no-store" });
السلوك: تُجلب البيانات في كل طلب.
الاستخدام: بيانات حية (أسعار، أخبار).
3. إعادة التحقق (ISR)
fetch(url, { next: { revalidate: 60 } }); // ← كل 60 ثانية
السلوك: تُجلب البيانات، وتُخزّن، وتُحدَّث كل 60 ثانية.
الاستخدام: بيانات تتغير بشكل دوري.
| الإعداد | السلوك | الاستخدام |
|---|---|---|
force-cache | تخزين دائم | بيانات ثابتة |
no-store | لا تخزين | بيانات حية |
revalidate: 60 | كل 60 ثانية | بيانات دورية |
Parallel Data Fetching
لجلب عدة مصادر بيانات بالتوازي:
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
إذا كانت البيانات تعتمد على بعضها:
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 تسمح بتحميل الصفحة بالتدريج.
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
لصفحة كاملة:
// 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
لمعالجة الأخطاء:
// 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:
"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:
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
"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
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
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
للبحث المباشر، أو البيانات التي تتغير بناءً على تفاعل المستخدم:
"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>
);
}
جدول أوقات جلب البيانات
| الأسلوب | المكان | التوقيت |
|---|---|---|
| Server + fetch | السيرفر | قبل العرض |
| Server Actions | السيرفر | عند إرسال النموذج |
| useEffect | المتصفح | بعد العرض |
| SWR | المتصفح | بعد العرض (مع cache) |
تمارين عملية
تمرين 1: جلب مستخدمين
اجلب قائمة مستخدمين من API.
الحل:
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
اجلب بيانات لا تُخزّن.
الحل:
const res = await fetch(url, { cache: "no-store" });
تمرين 3: ISR
اجلب بيانات مع إعادة تحقق كل 60 ثانية.
الحل:
const res = await fetch(url, { next: { revalidate: 60 } });
تمرين 4: Parallel Fetching
اجلب مصدرين بالتوازي.
الحل:
const [users, posts] = await Promise.all([
fetch("...").then((r) => r.json()),
fetch("...").then((r) => r.json()),
]);
تمرين 5: Suspense
أضف Suspense لمكون بطيء.
الحل:
<Suspense fallback={<p>جاري التحميل...</p>}>
<SlowComponent />
</Suspense>
تمرين 6: Server Action
أنشئ Server Action لإضافة عنصر.
الحل:
"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 يخزّن البيانات.
الحل:
fetch(url, { cache: "no-store" });
أو:
import { revalidatePath } from "next/cache";
revalidatePath("/posts");
🔴 المشكلة 2: fetch لا يعمل في Client
السبب: تحتاج URL كامل.
الحل: استخدم URL نسبي أو كامل:
fetch("/api/users"); // ← نسبي
fetch("https://api.example.com/users"); // ← كامل
🔴 المشكلة 3: Suspense لا يعمل
السبب: المكون ليس async.
الحل: تأكد من أن المكون async.
🔴 المشكلة 4: Server Action لا يعمل
السبب: لم تُضف "use server".
الحل: أضف في أعلى الملف.
🔴 المشكلة 5: revalidatePath is not a function
الحل:
import { revalidatePath } from "next/cache";
جدول أدوات البيانات
| الأداة | الاستخدام |
|---|---|
fetch | جلب البيانات |
cache: "no-store" | لا تخزين |
revalidate: 60 | ISR |
Promise.all | Parallel |
Suspense | Streaming |
Server Actions | البيانات المتغيرة |
revalidatePath | إعادة التحقق |
قائمة تحقق نهائية
| المهمة | الحالة |
|---|---|
| fetch في Server Component | ⬜ |
| cache: no-store | ⬜ |
| ISR (revalidate) | ⬜ |
| Parallel Fetching | ⬜ |
| Suspense | ⬜ |
| Server Actions | ⬜ |
| حل التمارين الثمانية | ⬜ |
ماذا بعد هذا المقال؟
الآن بعد أن أتقنت جلب البيانات، أنت جاهز للمقال التالي:
- API Routes — بناء APIs.
- Styling — Tailwind و CSS.
- Authentication — المصادقة.
الخلاصة
في هذا المقال، تعلمت:
- ✅ fetch في Server Component.
- ✅ cache (force, no-store, revalidate).
- ✅ Parallel vs Sequential.
- ✅ Suspense و Streaming.
- ✅ loading.tsx و error.tsx.
- ✅ Server Actions.
- ✅ useEffect في Client.
- ✅ حل 8 تمارين عملية.
تذكر: جلب البيانات هو قلب أي تطبيق. أتقنه جيداً.
Server Components في Next.js — دليل شامل 2026
API Routes في Next.js — دليل شامل 2026
📚 مقالات ذات صلة
مشروع Next.js متكامل — مدونة احترافية 2026
مشروع عملي شامل لبناء مدونة كاملة بـ Next.js — مع Markdown، SEO، نشر تلقائي، وتصميم احترافي
نشر تطبيق Next.js — دليل شامل 2026
دليل عملي مفصل لنشر تطبيق Next.js — Vercel، Firebase، Netlify، والتصدير الثابت
المصادقة في Next.js — دليل شامل 2026
دليل عملي مفصل للمصادقة في Next.js — NextAuth.js، JWT، Middleware، وحماية المسارات