🔥 App Router vs Pages Router: الفرق الحقيقي مش مجرد فولدر!
المقدمة: تغيير جذري مش مجرد Update
فيه ناس كتير أول ما شافت مجلد app/ في Next.js 13 قالت “آه، هما بس غيروا اسم الفولدر من pages لـ app وخلاص”، بس الحقيقة؟ ده أكبر تغيير في تاريخ Next.js والـ React نفسها.
الـ App Router مش مجرد نظام routing جديد. ده paradigm shift كامل في طريقة إنك تفكر في بناء تطبيقات الويب. الفكرة كلها اتغيرت من الأساس.
الفكرة الأساسية: Server-First بدل Client-First
Pages Router (الطريقة القديمة):
- كل الكود JavaScript بيروح للعميل (المتصفح)
- حتى لو بتعمل SSR، الكود بيروح كله للمتصفح
- React بيعمل “Hydration” للصفحة كلها
- النتيجة: باندل JavaScript كبير = بطء في التحميل
App Router (الطريقة الجديدة):
- الكود بيتنفذ على السيرفر أولاً
- بس اللي محتاج interaction بيروح للعميل
- React بتبعت HTML جاهز + شوية JavaScript للتفاعل
- النتيجة: باندل أصغر بكتير = سرعة وأداء أحسن
ليه التغيير ده حصل؟
لما React أعلنت عن React 18 ومعاها Server Components، كان ده تحدي كبير لـ Next.js. إزاي يدمجوا التقنية الجديدة دي في framework موجود من سنين؟
الحل: يعملوا نظام جديد تماماً = App Router
إيه اللي هتتعلمه في المقالة دي؟
في المقالة دي، مش بس هتعرف الفرق بين App Router و Pages Router. هتفهم بالظبط إزاي كل واحد بيشتغل من الداخل، مع أمثلة واقعية وتفصيلية على كل نقطة. هتعرف:
- ✅ إزاي React Server Components بتشتغل بالظبط
- ✅ الفرق بين Server و Client Components بالتفصيل الممل
- ✅ أنواع الـ Rendering المختلفة (Static, Dynamic, Streaming, ISR)
- ✅ نظام الكاش المعقد في App Router وإزاي تتحكم فيه
- ✅ Server Actions وإزاي تستخدمها صح
- ✅ Middleware والحاجات المتقدمة
- ✅ أمثلة واقعية كاملة (Authentication, E-commerce, Blog)
- ✅ كل المشاكل اللي ممكن تقابلك وإزاي تحلها
يلا بينا نبدأ رحلة الفهم العميق دي! 🚀
🎯 فهم React Server Components (RSC): الأساس اللي بني عليه كل حاجة
قبل ما ندخل في المقارنات، لازم نفهم الأول React Server Components لأنها الأساس اللي App Router مبني عليه. بدونها، مش هتفهم ليه التغيير ده أصلاً.
إيه هي React Server Components بالظبط؟
الفكرة ببساطة: في React التقليدية، كل المكونات (Components) بتشتغل في المتصفح (Client). حتى لو عملت Server-Side Rendering (SSR)، في الآخر كل الكود JavaScript بيروح للمتصفح عشان يعمل “Hydration”.
React Server Components بتقول: طب ليه نبعت كل الكود للعميل؟ خلينا نقسم المكونات لنوعين:
- Server Components - بتشتغل على السيرفر بس، مش بتروح للعميل خالص
- Client Components - بتشتغل في المتصفح زي الأول
مثال عملي يوضح الفرق:
الطريقة القديمة (كل حاجة Client):
// pages/blog.js - Pages Router
import { useState, useEffect } from 'react';
import { db } from '@/lib/db'; // ❌ لا! مش هينفع تستخدم DB هنا
export default function Blog() {
const [posts, setPosts] = useState([]);
useEffect(() => {
// محتاج تعمل API call
fetch('/api/posts')
.then(r => r.json())
.then(setPosts);
}, []);
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}
المشكلة:
- محتاج تعمل API route منفصل (
/api/posts) - محتاج تستخدم
useEffectوuseState - الكود بيتنفذ في المتصفح
- كل المكتبات بتروح للعميل
- Loading state محتاج تديره يدوي
الطريقة الجديدة (Server Component):
// app/blog/page.js - App Router
import { db } from '@/lib/db'; // ✅ ينفع! السيرفر بس هو اللي هيشوف ده
async function Blog() {
// ✅ اتصال مباشر بقاعدة البيانات!
const posts = await db.post.findMany({
select: { id: true, title: true, excerpt: true }
});
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}
export default Blog;
الفوايد:
- ✅ اتصال مباشر بالداتابيز من المكون نفسه!
- ✅ مفيش API route محتاجه
- ✅ مفيش
useStateأوuseEffect - ✅ المكتبات (زي Prisma/Drizzle) مش بتروح للعميل
- ✅ أبسط وأوضح وأسرع
إزاي RSC بتشتغل من الداخل؟
دي رحلة Request كاملة عشان تفهم بالظبط:
1. المستخدم يفتح /blog
Browser → Next.js Server
2. Next.js بتشغل Server Components على السيرفر
// على السيرفر:
const posts = await db.post.findMany(); // استعلام قاعدة البيانات
// بتجيب البيانات من DB
3. Next.js بتحول الـ Component لـ RSC Payload (تنسيق خاص)
RSC Payload = {
tree: { /* شجرة المكونات */ },
seedData: { /* البيانات */ }
}
4. Next.js بتبعت HTML + RSC Payload للمتصفح
<!-- HTML جاهز -->
<div>
<article>
<h2>عنوان المقال الأول</h2>
<p>نبذة مختصرة...</p>
</article>
<!-- باقي المقالات -->
</div>
<!-- + شوية بيانات للتفاعل (لو في Client Components) -->
5. المتصفح بيعرض HTML فوراً
النتيجة: المستخدم شاف المحتوى بسرعة! 🚀
الفرق عن SSR التقليدي:
- في SSR القديم: HTML جاهز ✅ + كل الكود JavaScript بيروح للعميل ❌
- في RSC: HTML جاهز ✅ + بس JavaScript اللي محتاجه للتفاعل ✅
Server Components vs Client Components: جدول المقارنة التفصيلي
| الميزة | Server Component | Client Component |
|---|---|---|
| مكان التنفيذ | السيرفر فقط | المتصفح (بعد Hydration) |
| اتصال بقاعدة البيانات | ✅ مباشر | ❌ لازم API |
| استخدام Secrets | ✅ آمن (API Keys, Tokens) | ❌ خطر (بيظهر للعميل) |
| حجم Bundle | لا يؤثر على Bundle | يزود حجم Bundle |
| React Hooks | ❌ (مفيش useState, useEffect…) | ✅ كل الـ Hooks |
| Event Handlers | ❌ (onClick, onChange…) | ✅ |
| Browser APIs | ❌ (localStorage, window…) | ✅ |
| استيراد مكتبات كبيرة | ✅ مجاناً (مش بتروح للعميل) | ❌ بتزود Bundle |
| SEO | ✅ ممتاز (محتوى جاهز) | ⚠️ يعتمد على الطريقة |
| الـ Loading | تلقائي مع Suspense | يدوي |
متى تستخدم Server Component؟
استخدم Server Component (الافتراضي) في الحالات دي:
// ✅ جلب بيانات من قاعدة البيانات
async function PostsList() {
const posts = await db.post.findMany();
return <div>{/* ... */}</div>;
}
// ✅ استخدام APIs خارجية
async function Weather() {
const data = await fetch('https://api.weather.com/...');
return <div>{/* ... */}</div>;
}
// ✅ عرض محتوى ثابت
function Header() {
return <header><h1>موقعي</h1></header>;
}
// ✅ استخدام secrets بأمان
async function Dashboard() {
const data = await fetch('https://api.example.com/data', {
headers: {
'Authorization': `Bearer ${process.env.SECRET_TOKEN}` // ✅ آمن
}
});
return <div>{/* ... */}</div>;
}
متى تستخدم Client Component؟
استخدم Client Component (مع "use client") في الحالات دي:
// ✅ التفاعل (onClick, onChange, onSubmit...)
'use client';
function Button() {
return <button onClick={() => alert('مرحبا!')}>اضغط</button>;
}
// ✅ State Management
'use client';
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// ✅ Effects
'use client';
import { useEffect } from 'react';
function Analytics() {
useEffect(() => {
trackPageView();
}, []);
return null;
}
// ✅ Browser APIs
'use client';
function ThemeToggle() {
const [theme, setTheme] = useState(() =>
localStorage.getItem('theme') || 'light'
);
return <button onClick={() => {
const newTheme = theme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
}}>Toggle</button>;
}
قاعدة مهمة جداً: Server Component ممكن يستورد Client Component (العكس لأ!)
// ✅ صح: Server Component → Client Component
// app/page.js (Server Component)
import ClientButton from './ClientButton'; // Client Component
export default function Page() {
return (
<div>
<h1>مرحباً</h1>
<ClientButton /> {/* ✅ ينفع */}
</div>
);
}
// ❌ غلط: Client Component → Server Component
// app/ClientComponent.js
'use client';
import ServerComponent from './ServerComponent'; // Server Component
export default function ClientComponent() {
return <ServerComponent />; // ❌ لا! مش هيشتغل
}
الحل للحالة الثانية: استخدم children prop:
// ✅ الحل الصحيح
// app/page.js (Server Component)
import ClientWrapper from './ClientWrapper';
import ServerContent from './ServerContent';
export default function Page() {
return (
<ClientWrapper>
<ServerContent /> {/* ✅ Server Component كـ children */}
</ClientWrapper>
);
}
// ClientWrapper.js
'use client';
export default function ClientWrapper({ children }) {
const [show, setShow] = useState(true);
return (
<div>
<button onClick={() => setShow(!show)}>Toggle</button>
{show && children}
</div>
);
}
🧱 الأول نرجع شوية: إيه اللي كان قبل كده؟
Pages Router: النظام القديم بالتفصيل
قبل Next.js 13، الدنيا كانت ماشية بنظام الـ Pages Router، واللي كان بسيط جداً:
البنية الأساسية:
my-app/
├── pages/
│ ├── _app.js ← Component أساسي بيلف كل الصفحات
│ ├── _document.js ← HTML Document الأساسي
│ ├── index.js ← الصفحة الرئيسية (/)
│ ├── about.js ← صفحة About (/about)
│ ├── blog/
│ │ ├── index.js ← قائمة المقالات (/blog)
│ │ └── [slug].js ← مقال معين (/blog/my-post)
│ └── api/
│ └── hello.js ← API Route (/api/hello)
└── public/ ← ملفات ثابتة
القواعد الأساسية:
- أي ملف جوه
pages/= Route مباشر - لو عايز صفحة جديدة، تعمل
about.jsوخلاص بقت/about - عايز تجيب بيانات؟ تستخدم واحدة من دول:
getStaticProps- للصفحات الثابتة (SSG)getServerSideProps- للصفحات الديناميكية (SSR)getStaticPaths- للـ Dynamic Routes مع SSG
مثال كامل على Pages Router:
// pages/blog/[slug].js
import { db } from '@/lib/db';
// بتجيب كل الـ paths الممكنة
export async function getStaticPaths() {
const posts = await db.post.findMany();
return {
paths: posts.map(post => ({
params: { slug: post.slug }
})),
fallback: 'blocking' // أو true أو false
};
}
// بتجيب البيانات لكل صفحة
export async function getStaticProps({ params }) {
const post = await db.post.findUnique({
where: { slug: params.slug }
});
return {
props: { post },
revalidate: 60 // ISR - تحديث كل 60 ثانية
};
}
// الـ Component نفسه
export default function BlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
المشاكل في Pages Router:
1. فصل بين جلب البيانات والعرض:
// Data fetching هنا
export async function getServerSideProps() {
const data = await fetchData();
return { props: { data } };
}
// UI component هنا (بعيد عن البيانات)
export default function Page({ data }) {
return <div>{data}</div>;
}
2. كل الكود بيروح للعميل:
// pages/products.js
import { db } from '@/lib/db'; // ❌ المكتبة دي بتروح للـ bundle!
export async function getServerSideProps() {
const products = await db.product.findMany(); // السيرفر بس
return { props: { products } };
}
export default function Products({ products }) {
// حتى لو المكون ده مش بيستخدم db
// المكتبة بتروح للـ bundle بسبب الـ import فوق
return <div>{/* ... */}</div>;
}
الحل القديم: لازم تفصل الملفات:
// lib/products.js - Server-side only
export async function getProducts() {
const { db } = await import('@/lib/db');
return await db.product.findMany();
}
// pages/products.js
import { getProducts } from '@/lib/products';
export async function getServerSideProps() {
const products = await getProducts();
return { props: { products } };
}
3. Layouts مش مرنة:
// pages/_app.js
function MyApp({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}
لو عايز layout مختلف لصفحات معينة؟ حل معقد!
4. مفيش Loading States تلقائية:
// كل ما الصفحة تتحمل، المستخدم بيستنى شاشة بيضا
// محتاج تعمل custom loading يدوي
5. مفيش Error Boundaries سهلة:
// محتاج تعمل Error Boundary يدوي لكل مكان
class ErrorBoundary extends React.Component {
// كود كتير...
}
ليه Pages Router كان كويس في وقته؟
مفهمناش غلط - Pages Router كان ثورة في وقته (2016-2022):
- ✅ بسيط وسهل التعلم
- ✅ File-based routing (فكرة عبقرية)
- ✅ SSR & SSG مدمجين
- ✅ API Routes
- ✅ Image Optimization
- ✅ Fast Refresh
بس… مع تطور React وظهور React 18، كان محتاج نقلة تانية.
💡 App Router: مش فولدر جديد، ده طريقة تفكير مختلفة
إزاي بتقسم التطبيق
الـ App Router بيعتمد على إنك تبني التطبيق بتقسيمة “Segments”، يعني كل مجلد جوّه app/ بيمثل جزء من المسار.
وجوا كل مجلد تقدر تحط ملفات خاصة:
| الملف | الوظيفة |
|---|---|
page.js | الصفحة الفعلية اللي بتتعرض |
layout.js | التخطيط المشترك (هيدر، سايدبار، إلخ) |
loading.js | شاشة التحميل للمسار ده |
error.js | معالجة الأخطاء |
not-found.js | لو المسار مش موجود |
template.js | للتحكم في إعادة العرض بين التنقلات |
route.js | API Routes |
يعني كل Route بقى مستقل بذاته، بكل اللي محتاجه — من UI لخطأ لتحميل.
مثال عملي على الهيكل:
app/
├── layout.js ← Layout رئيسي
├── page.js ← الصفحة الرئيسية (/)
├── about/
│ └── page.js ← صفحة About (/about)
├── blog/
│ ├── layout.js ← Layout خاص بالمدونة
│ ├── page.js ← قائمة المقالات (/blog)
│ ├── loading.js ← تحميل المدونة
│ ├── error.js ← معالجة أخطاء المدونة
│ └── [slug]/
│ └── page.js ← مقال معين (/blog/post-1)
└── dashboard/
├── layout.js ← Layout خاص بالـ Dashboard
├── page.js ← لوحة التحكم (/dashboard)
├── settings/
│ └── page.js ← الإعدادات (/dashboard/settings)
└── profile/
└── page.js ← الملف الشخصي (/dashboard/profile)
🎨 أنواع الـ Rendering في App Router: Static, Dynamic, Streaming و ISR
ده واحد من أهم المواضيع اللي لازم تفهمها كويس. App Router بيدعم 4 أنواع rendering مختلفة، وكل واحد ليه استخداماته.
1. Static Rendering (SSG) - الافتراضي والأسرع
ده الافتراضي! لو مكتبتش أي حاجة خاصة، Next.js بتعمل static rendering تلقائياً.
// app/about/page.js
// Static تلقائياً لأن مفيش dynamic data
export default function About() {
return (
<div>
<h1>من نحن</h1>
<p>معلومات عن الشركة...</p>
</div>
);
}
إزاي بتشتغل:
- وقت الـ Build: Next.js بتنفذ المكون وتولد HTML
- لما يجي request: بتبعت الـ HTML الجاهز فوراً (أسرع حاجة ممكنة!)
- مفيش سيرفر processing - الملف موجود جاهز
متى تستخدمها:
- ✅ صفحات ثابتة (About, Contact, Terms)
- ✅ Blog posts (مش بتتغير كتير)
- ✅ Documentation
- ✅ Landing pages
مثال مع بيانات:
// app/blog/page.js
async function BlogPage() {
// بتتنفذ وقت الـ Build مرة واحدة
const posts = await fetch('https://api.example.com/posts', {
cache: 'force-cache' // Static Cache (الافتراضي)
}).then(r => r.json());
return (
<div>
<h1>المدونة</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}
export default BlogPage;
في Pages Router كان:
// pages/blog.js
export async function getStaticProps() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return { props: { posts } };
}
export default function BlogPage({ posts }) {
return (/* ... */);
}
2. Dynamic Rendering (SSR) - لما تحتاج بيانات حديثة
لما البيانات بتتغير مع كل request (زي سلة المشتريات، الملف الشخصي…).
// app/cart/page.js
import { cookies } from 'next/headers';
async function CartPage() {
// استخدام cookies أو headers = Dynamic تلقائياً!
const cookieStore = await cookies();
const userId = cookieStore.get('userId');
// بتتنفذ مع كل request
const cart = await fetch(`https://api.example.com/cart/${userId}`, {
cache: 'no-store' // Dynamic - مفيش cache
}).then(r => r.json());
return (
<div>
<h1>سلة المشتريات</h1>
{cart.items.map(item => (
<div key={item.id}>{item.name} - {item.price} جنيه</div>
))}
</div>
);
}
export default CartPage;
إزاي بتشتغل:
- Request يجي للسيرفر
- السيرفر ينفذ المكون ويجيب البيانات
- يولد HTML ويبعته للعميل
- كل request = عملية جديدة
Next.js بتعمل Dynamic Rendering تلقائياً لو:
- استخدمت
cookies()أوheaders() - استخدمت
searchParamsفي Page - استخدمت
cache: 'no-store'في fetch - استخدمت Dynamic Functions
مثال آخر - صفحة البحث:
// app/search/page.js
async function SearchPage({ searchParams }) {
// searchParams = Dynamic تلقائياً
const { q } = await searchParams;
const results = await fetch(
`https://api.example.com/search?q=${q}`,
{ cache: 'no-store' }
).then(r => r.json());
return (
<div>
<h1>نتائج البحث عن: {q}</h1>
{results.map(result => (
<div key={result.id}>{result.title}</div>
))}
</div>
);
}
export default SearchPage;
Force Dynamic:
// app/dashboard/page.js
// لو عايز تجبر الصفحة تبقى dynamic
export const dynamic = 'force-dynamic';
// أو
export const revalidate = 0;
async function Dashboard() {
const data = await fetch('https://api.example.com/dashboard').then(r => r.json());
return <div>{/* ... */}</div>;
}
export default Dashboard;
في Pages Router كان:
// pages/cart.js
export async function getServerSideProps({ req }) {
const userId = req.cookies.userId;
const cart = await fetch(`https://api.example.com/cart/${userId}`).then(r => r.json());
return { props: { cart } };
}
export default function CartPage({ cart }) {
return (/* ... */);
}
3. Incremental Static Regeneration (ISR) - أحسن ما في العالمين
Static في الأول، بس بتتحدث كل فترة. أحسن حاجة للمدونات والأخبار!
// app/news/page.js
async function NewsPage() {
const news = await fetch('https://api.example.com/news', {
next: { revalidate: 60 } // تحديث كل 60 ثانية
}).then(r => r.json());
return (
<div>
<h1>آخر الأخبار</h1>
{news.map(article => (
<article key={article.id}>
<h2>{article.title}</h2>
<p>{article.summary}</p>
</article>
))}
</div>
);
}
export default NewsPage;
إزاي بتشتغل ISR:
Request 1 (0 seconds):
→ Static HTML موجود → بعته فوراً ✅
Request 2 (30 seconds):
→ Static HTML موجود → بعته فوراً ✅
Request 3 (65 seconds - بعد الـ 60 ثانية):
→ Static HTML موجود → بعته فوراً ✅
→ في الخلفية: بيجدد الصفحة 🔄
Request 4 (70 seconds):
→ HTML الجديد جاهز → بعته ✅
الفوايد:
- ✅ سرعة Static (المستخدم مش بيستنى)
- ✅ محتوى محدث (بيتجدد في الخلفية)
- ✅ توفير في السيرفر (مش كل request بيعمل rebuild)
مستويات مختلفة من Revalidation:
// 1. على مستوى الصفحة كلها
export const revalidate = 3600; // كل ساعة
// 2. على مستوى fetch معينة
async function ProductPage() {
// المنتج - تحديث كل 10 دقائق
const product = await fetch('https://api.example.com/product/1', {
next: { revalidate: 600 }
}).then(r => r.json());
// المراجعات - تحديث كل دقيقة
const reviews = await fetch('https://api.example.com/product/1/reviews', {
next: { revalidate: 60 }
}).then(r => r.json());
return (/* ... */);
}
On-Demand Revalidation - تحديث بناءً على حدث:
// app/api/revalidate/route.js
import { revalidatePath, revalidateTag } from 'next/cache';
export async function POST(request) {
const { path, tag } = await request.json();
if (path) {
revalidatePath(path); // تحديث صفحة معينة
}
if (tag) {
revalidateTag(tag); // تحديث كل الصفحات اللي عليها الـ tag ده
}
return Response.json({ revalidated: true });
}
استخدامها:
// app/blog/[slug]/page.js
async function BlogPost({ params }) {
const { slug } = await params;
const post = await fetch(`https://api.example.com/posts/${slug}`, {
next: {
tags: ['posts', `post-${slug}`] // Tags للـ revalidation
}
}).then(r => r.json());
return <article>{post.content}</article>;
}
// لما تعدل مقال:
// POST /api/revalidate
// { "tag": "post-my-article" }
// → الصفحة بتتحدث فوراً!
في Pages Router كان:
// pages/news.js
export async function getStaticProps() {
const news = await fetch('https://api.example.com/news').then(r => r.json());
return {
props: { news },
revalidate: 60
};
}
4. Streaming SSR - أحدث وأذكى نوع
الصفحة بتيجي تدريجياً - المستخدم بيشوف المحتوى جزء جزء مش بيستنى الصفحة كلها!
// app/dashboard/page.js
import { Suspense } from 'react';
export default function Dashboard() {
return (
<div>
{/* ده بيظهر فوراً */}
<h1>لوحة التحكم</h1>
{/* ده بيحمل في الخلفية */}
<Suspense fallback={<div>جاري تحميل الإحصائيات...</div>}>
<Analytics />
</Suspense>
{/* ده كمان بيحمل في الخلفية (بالتوازي مع Analytics) */}
<Suspense fallback={<div>جاري تحميل المبيعات...</div>}>
<Sales />
</Suspense>
</div>
);
}
async function Analytics() {
// بيانات بطيئة (3 ثواني)
const data = await fetch('https://api.example.com/analytics', {
next: { revalidate: 300 }
}).then(r => r.json());
return (
<div className="stats">
<h2>الإحصائيات</h2>
<p>الزيارات: {data.visits}</p>
</div>
);
}
async function Sales() {
// بيانات سريعة (1 ثانية)
const data = await fetch('https://api.example.com/sales').then(r => r.json());
return (
<div className="sales">
<h2>المبيعات</h2>
<p>المبيعات اليوم: {data.today} جنيه</p>
</div>
);
}
Timeline الحدوث:
0.0s → المستخدم يطلب /dashboard
0.1s → العنوان + Loading states بتظهر فوراً
┌─────────────────────────┐
│ لوحة التحكم │
├─────────────────────────┤
│ جاري تحميل الإحصائيات... │
│ جاري تحميل المبيعات... │
└─────────────────────────┘
1.0s → Sales خلصت تحميل → بتتحدث!
┌─────────────────────────┐
│ لوحة التحكم │
├─────────────────────────┤
│ جاري تحميل الإحصائيات... │
│ المبيعات │
│ المبيعات اليوم: 5000 │
└─────────────────────────┘
3.0s → Analytics خلصت تحميل → بتتحدث!
┌─────────────────────────┐
│ لوحة التحكم │
├─────────────────────────┤
│ الإحصائيات │
│ الزيارات: 1234 │
│ المبيعات │
│ المبيعات اليوم: 5000 │
└─────────────────────────┘
الفوايد:
- ✅ المستخدم مش بيستنى الصفحة كلها
- ✅ أسرع First Contentful Paint (FCP)
- ✅ البيانات السريعة بتظهر الأول - البطيئة متأخرتش الصفحة
- ✅ تجربة مستخدم أحسن بكتير
مثال واقعي - صفحة منتج في متجر:
// app/products/[id]/page.js
import { Suspense } from 'react';
export default async function ProductPage({ params }) {
const { id } = await params;
// المعلومات الأساسية (سريعة) - بتظهر فوراً
const product = await fetch(`https://api.example.com/products/${id}`, {
cache: 'force-cache'
}).then(r => r.json());
return (
<div>
{/* الجزء ده بيظهر فوراً */}
<h1>{product.name}</h1>
<img src={product.image} alt={product.name} />
<p className="price">{product.price} جنيه</p>
<button>أضف للسلة</button>
{/* المراجعات (ممكن تاخد وقت) */}
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews productId={id} />
</Suspense>
{/* المنتجات المشابهة */}
<Suspense fallback={<ProductsSkeleton />}>
<RelatedProducts categoryId={product.categoryId} />
</Suspense>
</div>
);
}
async function Reviews({ productId }) {
const reviews = await fetch(
`https://api.example.com/products/${productId}/reviews`
).then(r => r.json());
return (
<div className="reviews">
<h2>المراجعات ({reviews.length})</h2>
{reviews.map(review => (
<div key={review.id}>
<p>{review.comment}</p>
<span>⭐ {review.rating}</span>
</div>
))}
</div>
);
}
Pages Router: مكنش فيه Streaming! كنت بتستنى الصفحة كلها.
المقارنة الشاملة: أنواع الـ Rendering
| النوع | متى يتنفذ | السرعة | متى تستخدمه | Cache | في Pages Router |
|---|---|---|---|---|---|
| Static (SSG) | Build time | ⚡⚡⚡ الأسرع | محتوى ثابت | دائم | getStaticProps |
| Dynamic (SSR) | كل Request | ⚡ متوسط | بيانات متغيرة دايماً | لا يوجد | getServerSideProps |
| ISR | Build + خلفية | ⚡⚡ سريع جداً | محتوى بيتحدث بفترات | مؤقت | getStaticProps + revalidate |
| Streaming | Request تدريجي | ⚡⚡ جزئي سريع | صفحات معقدة | حسب الجزء | ❌ مفيش |
إزاي تختار النوع المناسب؟
سؤال نفسك:
البيانات بتتغير قد إيه؟
- مش بتتغير → Static (SSG)
- بتتغير كل شوية → ISR
- بتتغير مع كل request → Dynamic (SSR)
الصفحة فيها أجزاء بطيئة؟
- آه → Streaming مع Suspense
- لأ → شوف السؤال الأول
محتاج user-specific data؟ (cookies, auth…)
- آه → Dynamic (SSR)
- لأ → شوف السؤال الأول
مثال عملي - موقع أخبار:
// الصفحة الرئيسية - ISR (تحديث كل 5 دقائق)
// app/page.js
export const revalidate = 300;
async function HomePage() {
const news = await fetch('https://api.news.com/latest').then(r => r.json());
return (/* ... */);
}
// صفحة القسم - ISR (تحديث كل 10 دقائق)
// app/[category]/page.js
export const revalidate = 600;
async function CategoryPage({ params }) {
const { category } = await params;
const news = await fetch(`https://api.news.com/${category}`).then(r => r.json());
return (/* ... */);
}
// صفحة خبر معين - ISR + Streaming
// app/[category]/[slug]/page.js
export const revalidate = 3600; // ساعة
async function ArticlePage({ params }) {
const { slug } = await params;
// الخبر نفسه (سريع)
const article = await fetch(`https://api.news.com/articles/${slug}`).then(r => r.json());
return (
<article>
<h1>{article.title}</h1>
<div>{article.content}</div>
{/* التعليقات (ممكن تبطئ) */}
<Suspense fallback={<p>جاري تحميل التعليقات...</p>}>
<Comments articleId={article.id} />
</Suspense>
</article>
);
}
// الملف الشخصي - Dynamic (user-specific)
// app/profile/page.js
import { cookies } from 'next/headers';
async function ProfilePage() {
const cookieStore = await cookies();
const userId = cookieStore.get('userId');
const user = await fetch(`https://api.news.com/users/${userId}`, {
cache: 'no-store'
}).then(r => r.json());
return (/* ... */);
}
⚙️ جلب البيانات: تغيير كبير
Pages Router (الطريقة القديمة):
// pages/posts.js
export async function getServerSideProps() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return {
props: { posts }
};
}
export default function Posts({ posts }) {
return (
<div>
{posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
المشكلة: الكود متقسم، جزء لجلب البيانات وجزء للعرض.
App Router (الطريقة الجديدة):
// app/posts/page.js
async function Posts() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return (
<div>
{posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
export default Posts;
الفايدة: الكود كله في مكان واحد! أسهل وأوضح.
التحكم في الكاش:
// Cache لمدة 60 ثانية
const data = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 }
}).then(res => res.json());
// مفيش Cache خالص
const liveData = await fetch('https://api.example.com/live', {
cache: 'no-store'
}).then(res => res.json());
// Cache دائم (Static)
const staticData = await fetch('https://api.example.com/static', {
cache: 'force-cache'
}).then(res => res.json());
🧩 Layouts المتداخلة: ميزة جامدة
Pages Router: محدود شوية
// pages/_app.js
function MyApp({ Component, pageProps }) {
return (
<div>
<Header />
<Component {...pageProps} />
<Footer />
</div>
);
}
المشكلة: لو عايز layout معين لجزء بس من الصفحات؟ لازم تعمل حيل وكود معقد.
App Router: مرونة أكتر
// app/layout.js (Root Layout)
export default function RootLayout({ children }) {
return (
<html>
<body>
<Header />
{children}
<Footer />
</body>
</html>
);
}
// app/dashboard/layout.js (Dashboard Layout)
export default function DashboardLayout({ children }) {
return (
<div className="dashboard">
<Sidebar />
<main>{children}</main>
</div>
);
}
الفايدة الكبيرة:
لما تتنقل من /dashboard لـ /dashboard/settings:
- الـ Root Layout مش بيتعمله re-render
- الـ Dashboard Layout مش بيتعمله re-render
- بس الـ
page.jsالجديد هو اللي بيتحمل
يعني:
- ✅ السرعة بتبقى أسرع
- ✅ الـ State بيتحفظ (القوائم المفتوحة، Scroll Position…)
- ✅ التجربة smooth
⚡ الأداء: ليه App Router أسرع؟
1. Server Components = JavaScript أقل على العميل
// App Router - Server Component
import { db } from '@/lib/db'; // مثال: Prisma, Drizzle, إلخ
async function UsersList() {
const users = await db.user.findMany(); // أو db.query('SELECT * FROM users')
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
اللي بيوصل للعميل: HTML جاهز بس!
Pages Router: كل الكود + المكتبات بتروح للمتصفح.
مثال واقعي - Bundle Size:
Pages Router (الطريقة القديمة):
// pages/products.js
import { PrismaClient } from '@prisma/client'; // 500 KB! ❌
import { useState } from 'react';
const prisma = new PrismaClient();
export async function getServerSideProps() {
const products = await prisma.product.findMany();
return { props: { products } };
}
export default function ProductsPage({ products }) {
const [favorites, setFavorites] = useState([]);
return (
<div>
{products.map(product => (
<div key={product.id}>
<h2>{product.name}</h2>
<button onClick={() => toggleFavorite(product.id)}>
{favorites.includes(product.id) ? '❤️' : '🤍'}
</button>
</div>
))}
</div>
);
}
Bundle Size: ~520 KB (Prisma بتروح للعميل رغم إنها مش محتاجة! ❌)
App Router (الطريقة الجديدة):
// app/products/page.js (Server Component)
import { db } from '@/lib/db'; // ✅ مش بتروح للعميل!
import { FavoriteButton } from './FavoriteButton';
async function ProductsPage() {
const products = await db.product.findMany(); // Server-only
return (
<div>
{products.map(product => (
<div key={product.id}>
<h2>{product.name}</h2>
<FavoriteButton productId={product.id} />
</div>
))}
</div>
);
}
export default ProductsPage;
// app/products/FavoriteButton.js (Client Component)
'use client';
import { useState } from 'react';
export function FavoriteButton({ productId }) {
const [isFavorite, setIsFavorite] = useState(false);
return (
<button onClick={() => setIsFavorite(!isFavorite)}>
{isFavorite ? '❤️' : '🤍'}
</button>
);
}
Bundle Size: ~20 KB فقط! (500 KB أقل! 🚀)
2. Streaming Rendering
// app/page.js
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<h1>Welcome</h1>
<Suspense fallback={<p>Loading posts...</p>}>
<Posts />
</Suspense>
<Suspense fallback={<p>Loading comments...</p>}>
<Comments />
</Suspense>
</div>
);
}
النتيجة: المستخدم بيشوف المحتوى تدريجيًا، مش بيستنى الصفحة كلها.
3. مقارنة حجم الباندل:
| النوع | Pages Router | App Router |
|---|---|---|
| مشروع صغير | ~85 KB | ~45 KB |
| مشروع متوسط | ~220 KB | ~110 KB |
| مشروع كبير | ~500 KB+ | ~200 KB |
🔁 Loading و Error: تلقائي ومدمج
Pages Router: لازم تعمله يدوي
export default function Posts() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/posts')
.then(r => r.json())
.then(setPosts)
.catch(setError)
.finally(() => setLoading(false));
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return <div>{/* posts */}</div>;
}
الكود ده لازم تكرره في كل صفحة! تعب.
App Router: تلقائي وبسيط
// app/posts/loading.js
export default function Loading() {
return <div className="spinner">Loading posts...</div>;
}
// app/posts/error.js
'use client';
export default function Error({ error, reset }) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
// app/posts/page.js
async function Posts() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return <div>{/* posts */}</div>;
}
بس كده! Next.js بيتعامل مع كل حاجة تلقائياً.
💬 Routing API: Hooks جديدة
Pages Router:
import { useRouter } from 'next/router';
function MyComponent() {
const router = useRouter();
const { id, category } = router.query;
router.push('/about');
router.back();
}
App Router:
'use client';
import { useRouter, usePathname, useSearchParams, useParams } from 'next/navigation';
function MyComponent() {
const router = useRouter();
const pathname = usePathname(); // '/blog/post-1'
const searchParams = useSearchParams(); // ?sort=new&filter=tech
const params = useParams(); // { slug: 'post-1' }
// أكثر دقة وأسهل في الاستخدام
const sort = searchParams.get('sort');
const filter = searchParams.get('filter');
router.push('/about');
router.refresh();
router.prefetch('/products');
}
الحاجات الجديدة:
- ✅
usePathname()- تجيب المسار الحالي - ✅
useSearchParams()- للتعامل مع Query Parameters - ✅
useParams()- لجلب Dynamic Route Parameters - ✅
router.refresh()- لتحديث البيانات من السيرفر
🧾 SEO و Metadata: أسهل وأقوى
Pages Router:
import Head from 'next/head';
export default function Page() {
return (
<>
<Head>
<title>My Page Title</title>
<meta name="description" content="Page description" />
<meta property="og:image" content="/image.jpg" />
</Head>
<div>Content</div>
</>
);
}
App Router: Metadata API
// Static Metadata
export const metadata = {
title: 'My Page Title',
description: 'Page description',
openGraph: {
images: ['/image.jpg'],
type: 'website',
},
twitter: {
card: 'summary_large_image',
},
};
export default function Page() {
return <div>Content</div>;
}
// Dynamic Metadata
export async function generateMetadata({ params }) {
const { id } = await params;
const post = await fetch(`https://api.example.com/posts/${id}`).then(r => r.json());
return {
title: post.title,
description: post.excerpt,
openGraph: {
images: [post.image],
},
};
}
الفوايد:
- ✅ مفيش Import للـ
Head - ✅ أسهل وأنظف
- ✅ Type-safe مع TypeScript
- ✅ Dynamic Metadata سهلة
🧳 نظام الكاش في App Router: فهم عميق لكل المستويات
النظام ده معقد شوية لكن فهمه مهم جداً عشان تعرف تتحكم في الأداء. في Next.js App Router في 4 مستويات كاش مختلفة:
┌─────────────────────────────────────────────────┐
│ Next.js Caching System │
├─────────────────────────────────────────────────┤
│ 1. Request Memoization (في نفس الـ Render) │
│ 2. Data Cache (Server-side - دائم) │
│ 3. Full Route Cache (Server-side - HTML/RSC) │
│ 4. Router Cache (Client-side - في المتصفح) │
└─────────────────────────────────────────────────┘
المستوى 1: Request Memoization - تخزين مؤقت لنفس الـ Request
الفكرة: لو عملت نفس الـ fetch() أكتر من مرة في نفس الـ Request (نفس الـ Render)، Next.js بيستخدم نفس النتيجة.
// app/page.js
async function Header() {
// Request #1
const user = await fetch('https://api.example.com/user/123')
.then(r => r.json());
return <div>مرحباً {user.name}</div>;
}
async function Sidebar() {
// Request #2 - نفس الـ URL!
const user = await fetch('https://api.example.com/user/123')
.then(r => r.json());
return <div>البريد: {user.email}</div>;
}
async function Profile() {
// Request #3 - نفس الـ URL!
const user = await fetch('https://api.example.com/user/123')
.then(r => r.json());
return <div>العمر: {user.age}</div>;
}
export default function Page() {
return (
<>
<Header />
<Sidebar />
<Profile />
</>
);
}
اللي بيحصل:
- Request #1: بيعمل Fetch فعلي ← ✅
- Request #2: مش بيعمل Fetch تاني! بيستخدم نتيجة Request #1 ← ⚡ سريع!
- Request #3: كمان مش بيعمل Fetch! نفس النتيجة ← ⚡ سريع!
النتيجة: بدل 3 requests → request واحد بس!
متى ينفع؟
- ✅ Server Components فقط
- ✅ نفس الـ GET request
- ✅ نفس الـ URL بالظبط
- ✅ في نفس الـ Component Tree
متى لا ينفع؟
- ❌ Client Components
- ❌ POST/PUT/DELETE requests
- ❌ URLs مختلفة
- ❌ Requests مختلفة
لو عايز تعطله:
// استخدم AbortController مع signal مختلفة
const data = await fetch(url, {
signal: AbortSignal.timeout(5000),
cache: 'no-store' // ده بيعطل Data Cache، مش Request Memoization
});
المستوى 2: Data Cache - التخزين الدائم على السيرفر
الفكرة: النتائج بتتخزن على السيرفر حتى بعد الـ Request ينتهي. يعني لو 100 user زاروا الصفحة، بيستخدموا نفس الـ Cache.
// Cache دائم (افتراضي)
const posts = await fetch('https://api.example.com/posts', {
cache: 'force-cache' // الافتراضي
}).then(r => r.json());
Timeline:
Request 1 (User A - 10:00 AM):
→ Fetch من API ← يخزن في Cache
Request 2 (User B - 10:05 AM):
→ يجيب من Cache (مش من API!)
Request 3 (User C - 11:00 AM):
→ يجيب من Cache (مش من API!)
... إلخ (كل الـ Requests تستخدم نفس الـ Cache)
أنواع الـ Data Cache:
1. Cache دائم (Static)
const data = await fetch('https://api.example.com/about', {
cache: 'force-cache' // افتراضي - Cache دائم
});
متى تستخدمه: بيانات مش بتتغير أبداً (صفحة “من نحن”، شروط الاستخدام…)
2. بدون Cache (Dynamic)
const data = await fetch('https://api.example.com/live-data', {
cache: 'no-store' // بدون cache - كل request جديد
});
متى تستخدمه: بيانات بتتغير كل ثانية (الأسهم، الأسعار المباشرة، عدادات…)
مثال:
// app/stock/[symbol]/page.js
async function StockPage({ params }) {
const { symbol } = await params;
// أسعار الأسهم = No Cache (بتتغير كل ثانية)
const price = await fetch(
`https://api.stocks.com/price/${symbol}`,
{ cache: 'no-store' }
).then(r => r.json());
return (
<div>
<h1>{symbol}</h1>
<p className="price">{price.current} جنيه</p>
<p className="change" style={{ color: price.change > 0 ? 'green' : 'red' }}>
{price.change > 0 ? '↑' : '↓'} {price.changePercent}%
</p>
</div>
);
}
3. Time-based Revalidation (ISR)
const data = await fetch('https://api.example.com/news', {
next: { revalidate: 300 } // تحديث كل 5 دقائق
});
إزاي بتشتغل:
10:00 AM - Request 1:
→ Fetch من API ← يخزن في Cache (صالح لـ 5 دقائق)
10:02 AM - Request 2:
→ يجيب من Cache ✅ (لسه صالح)
10:06 AM - Request 3 (بعد 5 دقائق):
→ يجيب من Cache القديم ✅ (المستخدم مستنيش)
→ في الخلفية: Fetch جديد من API 🔄
10:07 AM - Request 4:
→ يجيب من Cache الجديد ✅
الفايدة: المستخدم دايماً بيشوف محتوى سريع، والبيانات بتتحدث في الخلفية.
مثال واقعي:
// app/news/page.js
async function NewsPage() {
// الأخبار الرئيسية - تحديث كل 5 دقائق
const headlines = await fetch('https://api.news.com/headlines', {
next: { revalidate: 300 }
}).then(r => r.json());
// المقالات الشائعة - تحديث كل ساعة
const trending = await fetch('https://api.news.com/trending', {
next: { revalidate: 3600 }
}).then(r => r.json());
return (
<div>
<section>
<h2>العناوين الرئيسية</h2>
{headlines.map(article => (
<article key={article.id}>{article.title}</article>
))}
</section>
<aside>
<h2>الأكثر قراءة</h2>
{trending.map(article => (
<article key={article.id}>{article.title}</article>
))}
</aside>
</div>
);
}
export default NewsPage;
4. Tag-based Revalidation (On-Demand)
الفكرة: عايز تحدث البيانات بناءً على حدث (مش وقت محدد).
// app/posts/[id]/page.js
async function PostPage({ params }) {
const { id } = await params;
const post = await fetch(`https://api.example.com/posts/${id}`, {
next: {
tags: ['posts', `post-${id}`] // Tags للتحديث
}
}).then(r => r.json());
return <article>{post.content}</article>;
}
التحديث On-Demand:
// app/api/revalidate/route.js
import { revalidateTag, revalidatePath } from 'next/cache';
export async function POST(request) {
const { type, value } = await request.json();
if (type === 'tag') {
// تحديث كل الصفحات اللي عليها الـ tag ده
revalidateTag(value);
}
if (type === 'path') {
// تحديث صفحة معينة
revalidatePath(value);
}
return Response.json({ revalidated: true, now: Date.now() });
}
مثال واقعي - CMS:
// Webhook من الـ CMS
// لما المحرر يعدل مقال:
// POST /api/revalidate
// { "type": "tag", "value": "post-123" }
// → الصفحة بتتحدث فوراً!
مثال كامل:
// app/blog/[slug]/page.js
export async function generateStaticParams() {
const posts = await fetch('https://cms.example.com/posts', {
next: { tags: ['all-posts'] }
}).then(r => r.json());
return posts.map(post => ({ slug: post.slug }));
}
async function BlogPost({ params }) {
const { slug } = await params;
const post = await fetch(`https://cms.example.com/posts/${slug}`, {
next: {
tags: ['posts', `post-${slug}`],
revalidate: 3600 // كمان ISR كـ backup (كل ساعة)
}
}).then(r => r.json());
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
export default BlogPost;
// لما المحرر يعدل المقال "my-first-post":
// POST /api/revalidate
// { "type": "tag", "value": "post-my-first-post" }
// → الصفحة تتحدث فوراً بدون ما المستخدم يعمل hard refresh!
المستوى 3: Full Route Cache - تخزين HTML كامل
الفكرة: Next.js بتخزن الصفحة كاملة (HTML + RSC Payload) على السيرفر.
للصفحات Static:
// app/about/page.js
export default function About() {
return <div>من نحن</div>;
}
// Next.js بتولد HTML مرة واحدة في الـ Build:
// .next/server/app/about.html ← جاهز!
التحكم في Route Cache:
// 1. Cache دائم (افتراضي للـ Static Routes)
export const dynamic = 'force-static';
// 2. بدون Cache (Dynamic)
export const dynamic = 'force-dynamic';
// 3. ISR
export const revalidate = 3600; // كل ساعة
// 4. Auto (Next.js تقرر)
export const dynamic = 'auto';
مثال:
// app/products/page.js
// الصفحة Static - بتتبني مرة واحدة
export const revalidate = 600; // تحديث كل 10 دقائق
async function ProductsPage() {
const products = await fetch('https://api.shop.com/products').then(r => r.json());
return (
<div className="products-grid">
{products.map(product => (
<div key={product.id} className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>{product.price} جنيه</p>
</div>
))}
</div>
);
}
export default ProductsPage;
اللي بيحصل:
- أول Build: Next.js بتنفذ المكون وتولد HTML
- Requests التالية: بتبعت HTML الجاهز فوراً (أسرع حاجة!)
- بعد 10 دقائق: بتحدث الـ HTML في الخلفية
- Requests الجديدة: بتاخد HTML المحدث
المستوى 4: Router Cache (Client-side) - في المتصفح
الفكرة: لما تتنقل بين الصفحات في المتصفح، Next.js بتخزن الصفحات اللي زرتها.
// المستخدم زار: / → /about → /blog
// Router Cache دلوقتي فيه:
{
'/': { ... },
'/about': { ... },
'/blog': { ... }
}
// لو رجع لـ / ← فوري! (من Cache)
المدة الافتراضية:
- Static Routes: 5 دقائق
- Dynamic Routes: 30 ثانية
التحكم في Router Cache:
'use client';
import { useRouter } from 'next/navigation';
function MyComponent() {
const router = useRouter();
return (
<>
{/* تحديث البيانات من السيرفر */}
<button onClick={() => router.refresh()}>
تحديث
</button>
{/* مسح الـ Cache كله */}
<button onClick={() => {
router.refresh();
// أو استخدم window.location.reload() للـ hard refresh
}}>
تحديث كامل
</button>
</>
);
}
جدول مقارنة شامل: مستويات الكاش الأربعة
| المستوى | المكان | المدة | الاستخدام | إزاي تعطله |
|---|---|---|---|---|
| Request Memoization | السيرفر | نفس الـ Request | تلقائي | مفيش طريقة مباشرة |
| Data Cache | السيرفر | دائم (أو حسب revalidate) | fetch() | cache: 'no-store' |
| Full Route Cache | السيرفر | دائم (أو حسب revalidate) | Static Routes | dynamic = 'force-dynamic' |
| Router Cache | المتصفح | 5 دقائق (Static) / 30 ثانية (Dynamic) | Navigation | router.refresh() |
سيناريو كامل: رحلة Request في النظام
السيناريو: User A يفتح /blog/my-post لأول مرة
┌────────────────────────────────────────────────────────┐
│ User A → /blog/my-post (أول مرة) │
└────────────────────────────────────────────────────────┘
↓
[Router Cache (Client)]
❌ مفيش - أول مرة
↓
Request → Next.js Server
↓
[Full Route Cache (Server)]
❌ مفيش (لو Dynamic) أو
✅ موجود (لو Static)
↓
[Execute Server Components]
↓
[Data Cache (Server)]
fetch('api.com/posts/my-post')
❌ مفيش → Fetch من API
✅ يخزن في Cache
↓
[Request Memoization]
لو نفس الـ fetch اتكرر في نفس الـ Render
✅ يستخدم نفس النتيجة
↓
HTML + RSC → Browser
↓
[Router Cache (Client)]
✅ يخزن الصفحة
User B يفتح نفس الصفحة بعد دقيقة:
┌────────────────────────────────────────────────────────┐
│ User B → /blog/my-post (بعد دقيقة) │
└────────────────────────────────────────────────────────┘
↓
Request → Next.js Server
↓
[Full Route Cache]
✅ موجود (لو Static)
→ بعت HTML فوراً ⚡⚡⚡
أو
[Data Cache]
✅ موجود → استخدمه
→ Generate HTML
→ بعت للـ Browser ⚡⚡
User A يرجع لنفس الصفحة (في نفس الـ Session):
┌────────────────────────────────────────────────────────┐
│ User A → back to /blog/my-post │
└────────────────────────────────────────────────────────┘
↓
[Router Cache (Client)]
✅ موجود!
→ عرض فوري بدون Request ⚡⚡⚡⚡
متى تستخدم إيه؟ Decision Tree
هل البيانات بتتغير؟
├─ لا (ثابتة)
│ └─ force-cache (افتراضي)
│ مثال: صفحة "من نحن"
│
├─ آه، بس نادراً (مرة في اليوم/الأسبوع)
│ └─ force-cache + revalidateTag
│ مثال: صفحات CMS
│
├─ آه، بانتظام (كل ساعة/يوم)
│ └─ revalidate: [seconds]
│ مثال: مدونة، أخبار
│
├─ آه، باستمرار (كل دقائق)
│ └─ revalidate: 60-300
│ مثال: منتديات، تعليقات
│
└─ آه، فوراً (real-time)
└─ cache: 'no-store'
مثال: أسعار، stocks، chat
مثال شامل: متجر إلكتروني
// app/products/page.js
// قائمة المنتجات - ISR (تحديث كل 10 دقائق)
export const revalidate = 600;
async function ProductsPage() {
const products = await fetch('https://api.shop.com/products', {
next: { tags: ['products'] }
}).then(r => r.json());
return (/* ... */);
}
// app/products/[id]/page.js
// صفحة منتج معين - ISR + Tags
export const revalidate = 1800; // 30 دقيقة
async function ProductPage({ params }) {
const { id } = await params;
// المنتج - مع Tag للتحديث on-demand
const product = await fetch(`https://api.shop.com/products/${id}`, {
next: {
tags: ['products', `product-${id}`],
revalidate: 1800
}
}).then(r => r.json());
return (
<div>
<h1>{product.name}</h1>
<p>{product.price} جنيه</p>
{/* المراجعات - تحديث أسرع */}
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews productId={id} />
</Suspense>
</div>
);
}
async function Reviews({ productId }) {
// المراجعات - تحديث كل 5 دقائق
const reviews = await fetch(
`https://api.shop.com/products/${productId}/reviews`,
{ next: { revalidate: 300 } }
).then(r => r.json());
return (/* ... */);
}
// app/cart/page.js
// سلة المشتريات - Dynamic (user-specific)
import { cookies } from 'next/headers';
export const dynamic = 'force-dynamic';
async function CartPage() {
const cookieStore = await cookies();
const userId = cookieStore.get('userId');
// بدون cache - البيانات خاصة بكل مستخدم
const cart = await fetch(`https://api.shop.com/cart/${userId}`, {
cache: 'no-store'
}).then(r => r.json());
return (/* ... */);
}
// app/api/revalidate/route.js
// لما المنتج يتعدل في الـ Admin Panel
import { revalidateTag } from 'next/cache';
export async function POST(request) {
const { productId } = await request.json();
// تحديث المنتج المعين
revalidateTag(`product-${productId}`);
// تحديث قائمة المنتجات
revalidateTag('products');
return Response.json({ revalidated: true });
}
في Pages Router: مكنش فيه المرونة دي! كنت بتختار بين Static أو SSR أو ISR بس، ومفيش Tag-based revalidation.
🎯 Parallel Routes و Intercepting Routes: ميزات متقدمة شوية
Parallel Routes
عرض أكتر من Route في نفس الوقت:
app/
├── layout.js
├── @team/
│ └── page.js
├── @analytics/
│ └── page.js
└── page.js
// app/layout.js
export default function Layout({ children, team, analytics }) {
return (
<div>
<div>{children}</div>
<div className="sidebar">
{team}
{analytics}
</div>
</div>
);
}
Intercepting Routes
فتح Modal من غير تغيير الـ URL:
app/
├── photos/
│ ├── page.js
│ └── [id]/
│ └── page.js
└── @modal/
└── (.)photos/
└── [id]/
└── page.js
النتيجة: لما تكليك على صورة، بتفتح في Modal، لكن لو refresh الصفحة بتفتح الصفحة الكاملة!
🛡️ Route Handlers: API Routes الجديدة
Pages Router:
// pages/api/posts.js
import { db } from '@/lib/db'; // مثال: Prisma, Drizzle, إلخ
export default async function handler(req, res) {
if (req.method === 'GET') {
const posts = await db.post.findMany();
res.status(200).json(posts);
}
}
App Router:
// app/api/posts/route.js
import { db } from '@/lib/db'; // مثال: Prisma, Drizzle, إلخ
export async function GET(request) {
const posts = await db.post.findMany();
return Response.json(posts);
}
export async function POST(request) {
const body = await request.json();
const newPost = await db.post.create({
data: body
});
return Response.json(newPost, { status: 201 });
}
export async function DELETE(request) {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
await db.post.delete({
where: { id }
});
return new Response(null, { status: 204 });
}
الفوايد:
- ✅ Web Standards (Request/Response API)
- ✅ أوضح وأسهل
- ✅ دعم أفضل للـ Streaming
🔄 التحديث من Pages لـ App Router: خطوة بخطوة
مش لازم تبدأ من الصفر!
Next.js بيسمحلك تشتغل بـ الاتنين مع بعض:
my-app/
├── pages/ ← Pages Router القديم
│ ├── index.js
│ └── about.js
├── app/ ← App Router الجديد
│ ├── layout.js
│ ├── page.js
│ └── blog/
│ └── page.js
خطوات التحديث:
1. إنشاء Root Layout
// app/layout.js
export const metadata = {
title: 'My App',
};
export default function RootLayout({ children }) {
return (
<html lang="ar" dir="rtl">
<body>{children}</body>
</html>
);
}
2. نقل صفحة بسيطة
قبل (Pages Router):
// pages/about.js
export default function About() {
return <div>About Page</div>;
}
بعد (App Router):
// app/about/page.js
export const metadata = {
title: 'About',
};
export default function About() {
return <div>About Page</div>;
}
3. نقل صفحة فيها Data Fetching
قبل:
// pages/posts.js
export async function getServerSideProps() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return { props: { posts } };
}
export default function Posts({ posts }) {
return <div>{posts.map(p => <div key={p.id}>{p.title}</div>)}</div>;
}
بعد:
// app/posts/page.js
async function Posts() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 }
}).then(r => r.json());
return <div>{posts.map(p => <div key={p.id}>{p.title}</div>)}</div>;
}
export default Posts;
4. نقل صفحة فيها State/Events
// app/counter/page.js
'use client'; // مهم جدًا!
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
5. نقل Dynamic Routes
قبل:
// pages/posts/[slug].js
export async function getStaticPaths() {
return {
paths: [{ params: { slug: 'post-1' } }],
fallback: true,
};
}
export async function getStaticProps({ params }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json());
return { props: { post } };
}
export default function Post({ post }) {
return <div>{post.title}</div>;
}
بعد:
// app/posts/[slug]/page.js
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return posts.map(post => ({ slug: post.slug }));
}
async function Post({ params }) {
const { slug } = await params;
const post = await fetch(`https://api.example.com/posts/${slug}`).then(r => r.json());
return <div>{post.title}</div>;
}
export default Post;
⚔️ مقارنة شاملة: Pages Router vs App Router
| البند | Pages Router | App Router |
|---|---|---|
| نوع المكونات | كلها Client افتراضيًا | Server افتراضيًا |
| جلب البيانات | getStaticProps, getServerSideProps | fetch() جوه المكون |
| التخطيطات | _app.js, _document.js | layout.js متداخل |
| Loading States | يدوي | loading.js تلقائي |
| Error Handling | يدوي | error.js تلقائي |
| الأداء | جيد | ممتاز |
| حجم الباندل | كبير نسبيًا | أصغر بكتير |
| التوجيه | next/router | next/navigation |
| SEO | next/head | Metadata API |
| الكاش | محدود | مرن جدًا |
| Streaming | ❌ | ✅ |
| Parallel Routes | ❌ (صعب) | ✅ مدمج |
| Intercepting Routes | ❌ | ✅ |
| API Routes | pages/api/ | app/api/route.js |
| Middleware | محدود | أقوى وأمرن |
| TypeScript | جيد | أفضل |
🎓 نصائح مهمة للـ App Router
1. استخدم Server Components أكتر ما تقدر
// ✅ Good: Server Component
import { db } from '@/lib/db'; // مثال: Prisma, Drizzle, إلخ
async function PostsList() {
const posts = await db.post.findMany();
return <div>{posts.map(p => <Post key={p.id} post={p} />)}</div>;
}
// ❌ Bad: Client Component بدون داعي
'use client';
import { useEffect, useState } from 'react';
function PostsList() {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch('https://api.example.com/posts')
.then(r => r.json())
.then(setPosts);
}, []);
return <div>{posts.map(p => <Post key={p.id} post={p} />)}</div>;
}
2. استخدم Suspense Boundaries عشان Loading States
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<Header />
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</div>
);
}
3. استفيد من جلب البيانات بالتوازي
// ✅ Good: Parallel
async function Page() {
const [posts, users] = await Promise.all([
fetch('https://api.example.com/posts').then(r => r.json()),
fetch('https://api.example.com/users').then(r => r.json()),
]);
return <div>...</div>;
}
// ❌ Bad: Sequential
async function Page() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
const users = await fetch('https://api.example.com/users').then(r => r.json());
return <div>...</div>;
}
4. خلي Client Components في الأوراق (Leaves) - يعني في آخر الشجرة
// ✅ Good Architecture
// app/posts/page.js (Server)
import { db } from '@/lib/db'; // مثال: Prisma, Drizzle, إلخ
async function PostsPage() {
const posts = await db.post.findMany();
return (
<div>
<h1>Posts</h1>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
// app/posts/PostCard.js (Server)
function PostCard({ post }) {
return (
<article>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
<LikeButton postId={post.id} />
</article>
);
}
// app/posts/LikeButton.js (Client)
'use client';
import { useState } from 'react';
function LikeButton({ postId }) {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'}
</button>
);
}
5. استخدم Route Groups عشان تنظم الكود
app/
├── (marketing)/
│ ├── about/
│ ├── contact/
│ └── layout.js
├── (shop)/
│ ├── products/
│ ├── cart/
│ └── layout.js
└── (admin)/
├── dashboard/
├── users/
└── layout.js
الفايدة: كل مجموعة ليها Layout خاص، لكن مش بتأثر على الـ URL!
🚨 مشاكل شائعة وإزاي تحلها
1. خطأ “You’re importing a component that needs useState”
المشكلة:
// app/page.js
import Counter from './Counter';
export default function Page() {
return <Counter />;
}
// Counter.js
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
الحل: بس تضيف 'use client' في الأول:
// Counter.js
'use client'; // أضف ده!
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
2. “Hydration failed” errors
السبب: HTML من السيرفر مختلف عن HTML من العميل.
الحل: استخدم useEffect عشان تتأكد إن الكومبوننت اتحمل على العميل:
'use client';
import { useEffect, useState } from 'react';
function TimeDisplay() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null; // أو skeleton
return <div>{new Date().toLocaleTimeString()}</div>;
}
3. Context Providers في Server Components
المشكلة:
// app/layout.js - لا يشتغل!
import { ThemeProvider } from './ThemeProvider';
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
);
}
الحل:
// app/providers.js
'use client';
import { ThemeProvider } from './ThemeProvider';
export function Providers({ children }) {
return <ThemeProvider>{children}</ThemeProvider>;
}
// app/layout.js
import { Providers } from './providers';
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>
{children}
</Providers>
</body>
</html>
);
}
4. Cookies و Headers في Server Components
// ✅ الطريقة الصحيحة
import { cookies, headers } from 'next/headers';
async function Profile() {
const cookieStore = await cookies();
const token = cookieStore.get('token');
const headersList = await headers();
const userAgent = headersList.get('user-agent');
return <div>...</div>;
}
5. Redirect من Server Component
import { redirect } from 'next/navigation';
async function ProtectedPage() {
const session = await getSession();
if (!session) {
redirect('/login');
}
return <div>Protected content</div>;
}
🚀 Server Actions: ثورة في التعامل مع Forms و Mutations
Server Actions واحدة من أقوى الميزات في App Router. الفكرة بتخليك تكتب server-side code مباشرة في المكونات بدون ما تحتاج API routes!
إيه هي Server Actions؟
ببساطة: دوال بتشتغل على السيرفر بس، تقدر تناديها من Client Components أو Server Components.
قبل Server Actions (Pages Router):
// 1. تعمل API Route
// pages/api/create-post.js
export default async function handler(req, res) {
if (req.method === 'POST') {
const { title, content } = req.body;
await db.post.create({ data: { title, content } });
res.json({ success: true });
}
}
// 2. تعمل Form
// pages/posts/new.js
'use client';
export default function NewPost() {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
await fetch('/api/create-post', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content })
});
router.push('/posts');
};
return (
<form onSubmit={handleSubmit}>
<input value={title} onChange={e => setTitle(e.target.value)} />
<textarea value={content} onChange={e => setContent(e.target.value)} />
<button type="submit">إنشاء</button>
</form>
);
}
مع Server Actions (App Router):
// app/posts/actions.js
'use server';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData) {
const title = formData.get('title');
const content = formData.get('content');
await db.post.create({
data: { title, content }
});
revalidatePath('/posts');
redirect('/posts');
}
// app/posts/new/page.js
import { createPost } from './actions';
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" />
<textarea name="content" />
<button type="submit">إنشاء</button>
</form>
);
}
الفرق:
- ✅ مفيش API route محتاجه
- ✅ مفيش fetch أو axios
- ✅ مفيش state management للـ form
- ✅ أبسط وأقصر بكتير
- ✅ Type-safe مع TypeScript
طرق استخدام Server Actions
1. Form Actions - الأبسط والأفضل
// app/todos/actions.js
'use server';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
export async function addTodo(formData) {
const text = formData.get('text');
await db.todo.create({
data: {
text,
completed: false,
createdAt: new Date()
}
});
revalidatePath('/todos');
}
export async function toggleTodo(formData) {
const id = formData.get('id');
const todo = await db.todo.findUnique({ where: { id } });
await db.todo.update({
where: { id },
data: { completed: !todo.completed }
});
revalidatePath('/todos');
}
export async function deleteTodo(formData) {
const id = formData.get('id');
await db.todo.delete({ where: { id } });
revalidatePath('/todos');
}
// app/todos/page.js
import { db } from '@/lib/db';
import { addTodo, toggleTodo, deleteTodo } from './actions';
async function TodosPage() {
const todos = await db.todo.findMany({
orderBy: { createdAt: 'desc' }
});
return (
<div>
<h1>قائمة المهام</h1>
{/* إضافة Todo */}
<form action={addTodo}>
<input
name="text"
placeholder="أضف مهمة جديدة..."
required
/>
<button type="submit">إضافة</button>
</form>
{/* عرض Todos */}
<ul>
{todos.map(todo => (
<li key={todo.id}>
{/* Toggle Todo */}
<form action={toggleTodo} style={{ display: 'inline' }}>
<input type="hidden" name="id" value={todo.id} />
<button type="submit">
{todo.completed ? '✅' : '⬜'}
</button>
</form>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
{/* Delete Todo */}
<form action={deleteTodo} style={{ display: 'inline' }}>
<input type="hidden" name="id" value={todo.id} />
<button type="submit">🗑️</button>
</form>
</li>
))}
</ul>
</div>
);
}
export default TodosPage;
الفوايد:
- ✅ Progressive Enhancement: بيشتغل حتى لو JavaScript مش شغال!
- ✅ مفيش Loading States محتاجها - Next.js بتديرها تلقائياً
- ✅ Revalidation تلقائية - الصفحة بتتحدث لوحدها
2. من Client Component - مع useFormStatus و useFormState
لما تحتاج تفاعل أكتر (loading state, error handling…):
// app/posts/actions.js
'use server';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
export async function createPost(prevState, formData) {
const title = formData.get('title');
const content = formData.get('content');
// Validation
if (!title || title.length < 3) {
return { error: 'العنوان يجب أن يكون 3 أحرف على الأقل' };
}
if (!content || content.length < 10) {
return { error: 'المحتوى قصير جداً' };
}
try {
await db.post.create({
data: { title, content }
});
revalidatePath('/posts');
return { success: 'تم إنشاء المقال بنجاح!' };
} catch (error) {
return { error: 'حدث خطأ أثناء الإنشاء' };
}
}
// app/posts/new/page.js
import { CreatePostForm } from './CreatePostForm';
export default function NewPostPage() {
return (
<div>
<h1>مقال جديد</h1>
<CreatePostForm />
</div>
);
}
// app/posts/new/CreatePostForm.js
'use client';
import { useFormState, useFormStatus } from 'react-dom';
import { createPost } from '../actions';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'جاري الإنشاء...' : 'إنشاء المقال'}
</button>
);
}
export function CreatePostForm() {
const [state, formAction] = useFormState(createPost, null);
return (
<form action={formAction}>
{state?.error && (
<div className="error">{state.error}</div>
)}
{state?.success && (
<div className="success">{state.success}</div>
)}
<div>
<label htmlFor="title">العنوان</label>
<input
id="title"
name="title"
required
/>
</div>
<div>
<label htmlFor="content">المحتوى</label>
<textarea
id="content"
name="content"
rows={10}
required
/>
</div>
<SubmitButton />
</form>
);
}
الفوايد:
- ✅
useFormStatus()- بتعرف لو الـ Form بيتبعت دلوقتي - ✅
useFormState()- بتدير الـ State والـ Errors - ✅ Progressive Enhancement - لسه شغال بدون JS!
3. استدعاء مباشر من Event Handler
// app/posts/actions.js
'use server';
import { db } from '@/lib/db';
export async function likePost(postId) {
await db.post.update({
where: { id: postId },
data: { likes: { increment: 1 } }
});
return { success: true };
}
// app/posts/[id]/LikeButton.js
'use client';
import { useState } from 'react';
import { likePost } from '../actions';
export function LikeButton({ postId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const [loading, setLoading] = useState(false);
const handleLike = async () => {
setLoading(true);
const result = await likePost(postId);
if (result.success) {
setLikes(likes + 1);
}
setLoading(false);
};
return (
<button onClick={handleLike} disabled={loading}>
❤️ {likes} {loading && '(جاري...)'}
</button>
);
}
مثال واقعي كامل: نظام تسجيل دخول
// app/auth/actions.js
'use server';
import { db } from '@/lib/db';
import bcrypt from 'bcryptjs';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { sign } from 'jsonwebtoken';
export async function register(prevState, formData) {
const email = formData.get('email');
const password = formData.get('password');
const name = formData.get('name');
// Validation
if (!email || !email.includes('@')) {
return { error: 'البريد الإلكتروني غير صحيح' };
}
if (!password || password.length < 8) {
return { error: 'كلمة المرور يجب أن تكون 8 أحرف على الأقل' };
}
// Check if user exists
const existingUser = await db.user.findUnique({
where: { email }
});
if (existingUser) {
return { error: 'البريد الإلكتروني مسجل بالفعل' };
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create user
const user = await db.user.create({
data: {
email,
name,
password: hashedPassword
}
});
// Create session
const token = sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
// Set cookie
const cookieStore = await cookies();
cookieStore.set('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7 // 7 days
});
redirect('/dashboard');
}
export async function login(prevState, formData) {
const email = formData.get('email');
const password = formData.get('password');
if (!email || !password) {
return { error: 'يرجى ملء جميع الحقول' };
}
const user = await db.user.findUnique({
where: { email }
});
if (!user) {
return { error: 'البريد الإلكتروني أو كلمة المرور خاطئة' };
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return { error: 'البريد الإلكتروني أو كلمة المرور خاطئة' };
}
// Create session
const token = sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
const cookieStore = await cookies();
cookieStore.set('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7
});
redirect('/dashboard');
}
export async function logout() {
const cookieStore = await cookies();
cookieStore.delete('token');
redirect('/login');
}
// app/register/page.js
import { RegisterForm } from './RegisterForm';
export default function RegisterPage() {
return (
<div className="auth-page">
<h1>إنشاء حساب جديد</h1>
<RegisterForm />
</div>
);
}
// app/register/RegisterForm.js
'use client';
import { useFormState, useFormStatus } from 'react-dom';
import { register } from '../auth/actions';
import Link from 'next/link';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending} className="btn-primary">
{pending ? 'جاري التسجيل...' : 'تسجيل'}
</button>
);
}
export function RegisterForm() {
const [state, formAction] = useFormState(register, null);
return (
<form action={formAction} className="auth-form">
{state?.error && (
<div className="error-message">{state.error}</div>
)}
<div className="form-group">
<label htmlFor="name">الاسم</label>
<input
id="name"
name="name"
type="text"
required
autoComplete="name"
/>
</div>
<div className="form-group">
<label htmlFor="email">البريد الإلكتروني</label>
<input
id="email"
name="email"
type="email"
required
autoComplete="email"
/>
</div>
<div className="form-group">
<label htmlFor="password">كلمة المرور</label>
<input
id="password"
name="password"
type="password"
required
autoComplete="new-password"
minLength={8}
/>
</div>
<SubmitButton />
<p className="auth-link">
لديك حساب؟ <Link href="/login">تسجيل الدخول</Link>
</p>
</form>
);
}
// app/dashboard/page.js
import { cookies } from 'next/headers';
import { verify } from 'jsonwebtoken';
import { redirect } from 'next/navigation';
import { db } from '@/lib/db';
import { LogoutButton } from './LogoutButton';
async function DashboardPage() {
const cookieStore = await cookies();
const token = cookieStore.get('token');
if (!token) {
redirect('/login');
}
try {
const decoded = verify(token.value, process.env.JWT_SECRET);
const user = await db.user.findUnique({
where: { id: decoded.userId }
});
if (!user) {
redirect('/login');
}
return (
<div>
<h1>مرحباً، {user.name}!</h1>
<p>البريد الإلكتروني: {user.email}</p>
<LogoutButton />
</div>
);
} catch (error) {
redirect('/login');
}
}
export default DashboardPage;
// app/dashboard/LogoutButton.js
'use client';
import { logout } from '../auth/actions';
export function LogoutButton() {
return (
<form action={logout}>
<button type="submit" className="btn-danger">
تسجيل الخروج
</button>
</form>
);
}
Server Actions مع File Upload
// app/upload/actions.js
'use server';
import { db } from '@/lib/db';
import { writeFile } from 'fs/promises';
import { join } from 'path';
export async function uploadImage(formData) {
const file = formData.get('image');
if (!file) {
return { error: 'لم يتم اختيار ملف' };
}
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
// Generate unique filename
const filename = `${Date.now()}-${file.name}`;
const path = join(process.cwd(), 'public', 'uploads', filename);
await writeFile(path, buffer);
// Save to database
await db.image.create({
data: {
filename,
originalName: file.name,
size: file.size,
mimeType: file.type,
url: `/uploads/${filename}`
}
});
return { success: true, url: `/uploads/${filename}` };
}
// app/upload/page.js
import { UploadForm } from './UploadForm';
export default function UploadPage() {
return (
<div>
<h1>رفع صورة</h1>
<UploadForm />
</div>
);
}
// app/upload/UploadForm.js
'use client';
import { useFormState, useFormStatus } from 'react-dom';
import { uploadImage } from './actions';
import { useState } from 'react';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'جاري الرفع...' : 'رفع الصورة'}
</button>
);
}
export function UploadForm() {
const [state, formAction] = useFormState(uploadImage, null);
const [preview, setPreview] = useState(null);
const handleFileChange = (e) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setPreview(reader.result);
};
reader.readAsDataURL(file);
}
};
return (
<form action={formAction}>
{state?.error && (
<div className="error">{state.error}</div>
)}
{state?.success && (
<div className="success">
تم رفع الصورة بنجاح!
<img src={state.url} alt="Uploaded" />
</div>
)}
<div>
<input
type="file"
name="image"
accept="image/*"
onChange={handleFileChange}
required
/>
</div>
{preview && (
<div className="preview">
<img src={preview} alt="Preview" style={{ maxWidth: '300px' }} />
</div>
)}
<SubmitButton />
</form>
);
}
Best Practices لـ Server Actions
1. Validation
'use server';
import { z } from 'zod';
const PostSchema = z.object({
title: z.string().min(3).max(100),
content: z.string().min(10),
published: z.boolean().optional()
});
export async function createPost(formData) {
const rawData = {
title: formData.get('title'),
content: formData.get('content'),
published: formData.get('published') === 'on'
};
const result = PostSchema.safeParse(rawData);
if (!result.success) {
return {
error: result.error.flatten().fieldErrors
};
}
const post = await db.post.create({
data: result.data
});
return { success: true, post };
}
2. Authentication
'use server';
import { cookies } from 'next/headers';
import { verify } from 'jsonwebtoken';
async function getCurrentUser() {
const cookieStore = await cookies();
const token = cookieStore.get('token');
if (!token) {
throw new Error('Not authenticated');
}
const decoded = verify(token.value, process.env.JWT_SECRET);
return decoded;
}
export async function deletePost(postId) {
const user = await getCurrentUser();
const post = await db.post.findUnique({
where: { id: postId }
});
if (post.authorId !== user.userId) {
return { error: 'غير مصرح لك بحذف هذا المقال' };
}
await db.post.delete({
where: { id: postId }
});
return { success: true };
}
3. Error Handling
'use server';
export async function createPost(formData) {
try {
const title = formData.get('title');
const content = formData.get('content');
const post = await db.post.create({
data: { title, content }
});
revalidatePath('/posts');
return { success: true, post };
} catch (error) {
console.error('Error creating post:', error);
if (error.code === 'P2002') {
return { error: 'المقال موجود بالفعل' };
}
return { error: 'حدث خطأ أثناء إنشاء المقال' };
}
}
Server Actions vs API Routes: متى تستخدم إيه؟
استخدم Server Actions لو:
- ✅ Form submissions
- ✅ Mutations (Create, Update, Delete)
- ✅ User interactions (like, follow, bookmark…)
- ✅ Progressive Enhancement مهمة
استخدم API Routes لو:
- ✅ Webhooks (من خدمات خارجية)
- ✅ Public API (للاستخدام من خارج Next.js)
- ✅ Complex logic مش مرتبطة بـ UI
- ✅ Third-party integrations
🔥 ميزات متقدمة في App Router (اختياري)
1. Route Handlers مع Streaming
// app/api/stream/route.js
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
await new Promise(resolve => setTimeout(resolve, 1000));
controller.enqueue(encoder.encode(`data: ${i}\n\n`));
}
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
},
});
}
3. Partial Prerendering (PPR)
// next.config.js
module.exports = {
experimental: {
ppr: true,
},
};
// app/page.js
import { Suspense } from 'react';
export default function Page() {
return (
<div>
{/* Static content - يتعرض فورًا */}
<Header />
{/* Dynamic content - يتحمل لاحقًا */}
<Suspense fallback={<Skeleton />}>
<DynamicContent />
</Suspense>
{/* Static footer */}
<Footer />
</div>
);
}
4. Draft Mode
// app/api/draft/route.js
import { draftMode } from 'next/headers';
export async function GET(request) {
(await draftMode()).enable();
return new Response('Draft mode enabled');
}
// app/posts/[slug]/page.js
import { draftMode } from 'next/headers';
async function Post({ params }) {
const { slug } = await params;
const isDraft = (await draftMode()).isEnabled;
const post = await fetch(`https://api.example.com/posts/${slug}`, {
cache: isDraft ? 'no-store' : 'force-cache',
}).then(r => r.json());
return (
<article>
{isDraft && <div className="draft-banner">Draft Mode</div>}
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
🎯 متى تستخدم إيه؟
استخدم Pages Router لو:
- ✅ مشروع قديم شغال زي الفل ومفيش داعي للتغيير
- ✅ الفريق مش جاهز للـ paradigm shift
- ✅ عندك dependencies قديمة مش متوافقة مع RSC
- ✅ المشروع بسيط جدًا ومش محتاج الميزات الجديدة
استخدم App Router لو:
- ✅ مشروع جديد
- ✅ محتاج أداء عالي جدًا
- ✅ محتاج SEO قوي
- ✅ عايز تستفيد من أحدث تقنيات React
- ✅ المشروع هيكبر مع الوقت
- ✅ محتاج تجربة مستخدم سلسة
🛠️ أدوات مساعدة للتحديث
1. Next.js Codemod
# تحويل تلقائي لبعض الأكواد
npx @next/codemod@latest app-router-migration ./pages
2. ESLint Plugin
npm install eslint-plugin-next
// .eslintrc.json
{
"extends": ["next/core-web-vitals"],
"rules": {
"next/no-async-client-component": "error"
}
}
3. TypeScript Config
// tsconfig.json
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"jsx": "preserve",
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
🛡️ Middleware في App Router: التحكم الكامل في Requests
Middleware بتخليك تشغل كود قبل ما الـ Request يوصل للصفحة. ده مفيد جداً للـ Authentication، Redirects، Logging، وحاجات تانية كتير.
إيه هي Middleware؟
ببساطة: دالة بتشتغل على Edge (قريب من المستخدم جداً) قبل كل request، وبتقدر تعدل الـ Response أو تعمل Redirect أو تضيف Headers.
البنية الأساسية
// middleware.js (في الـ root بجانب app/)
import { NextResponse } from 'next/server';
export function middleware(request) {
// الكود بتاعك هنا
return NextResponse.next();
}
// اختياري: حدد الـ paths اللي Middleware بتشتغل عليها
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*']
};
مهم:
- الملف اسمه
middleware.jsأوmiddleware.ts - لازم يكون في الـ root (جنب
app/) - بيشتغل على Edge Runtime (مش Node.js!)
مثال 1: Authentication Middleware
// middleware.js
import { NextResponse } from 'next/server';
import { verify } from '@/lib/jwt'; // مكتبة JWT lightweight
export async function middleware(request) {
const token = request.cookies.get('token');
const { pathname } = request.nextUrl;
// الصفحات المحمية
const protectedPaths = ['/dashboard', '/profile', '/settings'];
const isProtectedPath = protectedPaths.some(path =>
pathname.startsWith(path)
);
// لو صفحة محمية ومفيش token
if (isProtectedPath && !token) {
const url = request.nextUrl.clone();
url.pathname = '/login';
url.searchParams.set('from', pathname); // عشان نرجعه بعد Login
return NextResponse.redirect(url);
}
// لو فيه token، نتأكد إنه صحيح
if (token) {
try {
const user = await verify(token.value);
// نضيف User info في الـ Headers (للصفحة تستخدمها)
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', user.id);
requestHeaders.set('x-user-email', user.email);
return NextResponse.next({
request: {
headers: requestHeaders
}
});
} catch (error) {
// Token مش صحيح
const response = NextResponse.redirect(
new URL('/login', request.url)
);
response.cookies.delete('token'); // امسح الـ Token الغلط
return response;
}
}
return NextResponse.next();
}
export const config = {
matcher: [
'/dashboard/:path*',
'/profile/:path*',
'/settings/:path*'
]
};
استخدام User info في الصفحة:
// app/dashboard/page.js
import { headers } from 'next/headers';
export default async function DashboardPage() {
const headersList = await headers();
const userId = headersList.get('x-user-id');
const userEmail = headersList.get('x-user-email');
return (
<div>
<h1>Dashboard</h1>
<p>User ID: {userId}</p>
<p>Email: {userEmail}</p>
</div>
);
}
مثال 2: Role-Based Access Control (RBAC)
// middleware.js
import { NextResponse } from 'next/server';
import { verify } from '@/lib/jwt';
const ROLE_PATHS = {
admin: ['/admin'],
editor: ['/editor', '/posts/new'],
user: ['/dashboard', '/profile']
};
export async function middleware(request) {
const token = request.cookies.get('token');
const { pathname } = request.nextUrl;
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
try {
const user = await verify(token.value);
// تحقق من الصلاحيات
const allowedPaths = ROLE_PATHS[user.role] || [];
const hasAccess = allowedPaths.some(path =>
pathname.startsWith(path)
);
if (!hasAccess) {
// مش مصرح ليه
return NextResponse.redirect(new URL('/forbidden', request.url));
}
// أضف User info للـ Headers
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', user.id);
requestHeaders.set('x-user-role', user.role);
return NextResponse.next({
request: { headers: requestHeaders }
});
} catch (error) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
export const config = {
matcher: ['/admin/:path*', '/editor/:path*', '/dashboard/:path*']
};
مثال 3: Internationalization (i18n)
// middleware.js
import { NextResponse } from 'next/server';
const locales = ['ar', 'en', 'fr'];
const defaultLocale = 'ar';
function getLocale(request) {
// 1. تحقق من الـ Cookie
const localeCookie = request.cookies.get('locale');
if (localeCookie && locales.includes(localeCookie.value)) {
return localeCookie.value;
}
// 2. تحقق من الـ Accept-Language header
const acceptLanguage = request.headers.get('accept-language');
if (acceptLanguage) {
const lang = acceptLanguage.split(',')[0].split('-')[0];
if (locales.includes(lang)) {
return lang;
}
}
// 3. اللغة الافتراضية
return defaultLocale;
}
export function middleware(request) {
const { pathname } = request.nextUrl;
// تحقق لو الـ path فيه locale بالفعل
const pathnameHasLocale = locales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (pathnameHasLocale) {
return NextResponse.next();
}
// أضف الـ locale للـ path
const locale = getLocale(request);
const url = request.nextUrl.clone();
url.pathname = `/${locale}${pathname}`;
const response = NextResponse.redirect(url);
response.cookies.set('locale', locale, { maxAge: 60 * 60 * 24 * 365 });
return response;
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)'
]
};
البنية الجديدة:
app/
├── [locale]/
│ ├── layout.js
│ ├── page.js
│ ├── about/
│ │ └── page.js
│ └── blog/
│ └── page.js
└── api/
مثال 4: Rate Limiting
// lib/rate-limit.js
const rateLimit = new Map();
export function checkRateLimit(ip, limit = 10, window = 60000) {
const now = Date.now();
const userRequests = rateLimit.get(ip) || [];
// امسح الـ Requests القديمة
const recentRequests = userRequests.filter(time => now - time < window);
if (recentRequests.length >= limit) {
return false; // تخطى الحد
}
recentRequests.push(now);
rateLimit.set(ip, recentRequests);
return true;
}
// middleware.js
import { NextResponse } from 'next/server';
import { checkRateLimit } from '@/lib/rate-limit';
export function middleware(request) {
const ip = request.ip || request.headers.get('x-forwarded-for') || 'unknown';
const { pathname } = request.nextUrl;
// Rate limit للـ API routes
if (pathname.startsWith('/api/')) {
const allowed = checkRateLimit(ip, 100, 60000); // 100 requests/دقيقة
if (!allowed) {
return new NextResponse(
JSON.stringify({ error: 'Too many requests' }),
{
status: 429,
headers: {
'Content-Type': 'application/json',
'Retry-After': '60'
}
}
);
}
}
return NextResponse.next();
}
export const config = {
matcher: '/api/:path*'
};
مثال 5: A/B Testing
// middleware.js
import { NextResponse } from 'next/server';
const EXPERIMENTS = {
'new-homepage': {
variants: ['control', 'variant-a', 'variant-b'],
weights: [0.34, 0.33, 0.33] // التوزيع
}
};
function getVariant(experimentId, request) {
// تحقق من الـ Cookie
const cookie = request.cookies.get(`exp_${experimentId}`);
if (cookie) {
return cookie.value;
}
// اختر variant جديد
const experiment = EXPERIMENTS[experimentId];
const random = Math.random();
let sum = 0;
for (let i = 0; i < experiment.variants.length; i++) {
sum += experiment.weights[i];
if (random <= sum) {
return experiment.variants[i];
}
}
return experiment.variants[0];
}
export function middleware(request) {
const { pathname } = request.nextUrl;
if (pathname === '/') {
const variant = getVariant('new-homepage', request);
const response = NextResponse.next();
response.cookies.set(`exp_new-homepage`, variant, {
maxAge: 60 * 60 * 24 * 30 // شهر
});
// أضف Header عشان الصفحة تعرف الـ Variant
response.headers.set('x-experiment-variant', variant);
return response;
}
return NextResponse.next();
}
export const config = {
matcher: '/'
};
الصفحة:
// app/page.js
import { headers } from 'next/headers';
import { ControlHomepage } from './variants/control';
import { VariantAHomepage } from './variants/variant-a';
import { VariantBHomepage } from './variants/variant-b';
export default async function HomePage() {
const headersList = await headers();
const variant = headersList.get('x-experiment-variant');
switch (variant) {
case 'variant-a':
return <VariantAHomepage />;
case 'variant-b':
return <VariantBHomepage />;
default:
return <ControlHomepage />;
}
}
مثال 6: Bot Detection & Blocking
// middleware.js
import { NextResponse } from 'next/server';
const BOT_PATTERNS = [
/bot/i,
/crawler/i,
/spider/i,
/scraper/i
];
const ALLOWED_BOTS = [
'Googlebot',
'Bingbot',
'facebookexternalhit'
];
function isBot(userAgent) {
return BOT_PATTERNS.some(pattern => pattern.test(userAgent));
}
function isAllowedBot(userAgent) {
return ALLOWED_BOTS.some(bot => userAgent.includes(bot));
}
export function middleware(request) {
const userAgent = request.headers.get('user-agent') || '';
const { pathname } = request.nextUrl;
// الصفحات اللي عاوزين نحميها من Bots
const protectedPaths = ['/api/submit', '/api/contact'];
const isProtected = protectedPaths.some(path => pathname.startsWith(path));
if (isProtected && isBot(userAgent) && !isAllowedBot(userAgent)) {
return new NextResponse('Forbidden', { status: 403 });
}
return NextResponse.next();
}
export const config = {
matcher: '/api/:path*'
};
مثال 7: Geolocation Redirects
// middleware.js
import { NextResponse } from 'next/server';
const COUNTRY_DOMAINS = {
'US': 'https://us.example.com',
'GB': 'https://uk.example.com',
'DE': 'https://de.example.com',
'EG': 'https://eg.example.com'
};
export function middleware(request) {
// Vercel بتوفر الـ Geo info في الـ Headers
const country = request.geo?.country || 'US';
const targetDomain = COUNTRY_DOMAINS[country];
// لو المستخدم مش على الدومين المناسب
if (targetDomain && !request.url.startsWith(targetDomain)) {
return NextResponse.redirect(targetDomain + request.nextUrl.pathname);
}
return NextResponse.next();
}
Middleware Matcher Patterns - أمثلة متقدمة
// 1. كل الصفحات ماعدا الـ Static files
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico).*)'
]
};
// 2. صفحات معينة بس
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*']
};
// 3. كل حاجة ماعدا الـ API
export const config = {
matcher: [
'/((?!api).*)'
]
};
// 4. Multiple matchers
export const config = {
matcher: [
'/dashboard/:path*',
'/admin/:path*',
'/((?!_next/static|_next/image|favicon.ico)api/.*)'
]
};
Edge Runtime Limitations
Middleware بتشتغل على Edge Runtime، فمش كل حاجة متاحة:
✅ متاح:
fetch()و Web APIs- Crypto APIs
- Headers, Cookies manipulation
- URL manipulation
- JSON operations
- Light libraries (jose, nanoid…)
❌ مش متاح:
fs(File System)process.env(استخدمenvvariables مباشرة)- Database connections (Prisma, Mongoose…)
- Heavy npm packages
- Node.js APIs
الحل: لو محتاج حاجات تقيلة، استخدم Server Components أو API Routes.
Best Practices
1. خلي Middleware خفيفة
// ❌ سيء - عمليات تقيلة
export async function middleware(request) {
const users = await db.user.findMany(); // ❌ لا!
const posts = await fetchAllPosts(); // ❌ لا!
// ...
}
// ✅ كويس - عمليات خفيفة
export async function middleware(request) {
const token = request.cookies.get('token');
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
2. استخدم Matcher بذكاء
// ❌ سيء - Middleware بتشتغل على كل حاجة
export const config = {
matcher: '/:path*'
};
// ✅ كويس - بس الصفحات اللي محتاجة
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*']
};
3. Handle Errors
export async function middleware(request) {
try {
const token = request.cookies.get('token');
const user = await verify(token.value);
return NextResponse.next();
} catch (error) {
console.error('Middleware error:', error);
return NextResponse.redirect(new URL('/login', request.url));
}
}
Middleware vs Server Components: متى تستخدم إيه؟
استخدم Middleware لو:
- ✅ Redirects قبل الصفحة تحمل
- ✅ Authentication checks سريعة
- ✅ Headers manipulation
- ✅ Cookies manipulation
- ✅ Rate limiting
- ✅ A/B Testing
- ✅ Bot detection
استخدم Server Components لو:
- ✅ Database queries
- ✅ Complex logic
- ✅ Heavy computations
- ✅ API calls لخدمات خارجية
- ✅ File operations
مثال واقعي كامل: Middleware للـ Authentication + RBAC + Logging
// middleware.js
import { NextResponse } from 'next/server';
import { verify } from '@/lib/jwt';
const PUBLIC_PATHS = ['/login', '/register', '/forgot-password', '/'];
const ROLE_ACCESS = {
admin: ['/admin', '/dashboard', '/users', '/settings'],
user: ['/dashboard', '/profile', '/settings']
};
// Logging helper
function log(message, data = {}) {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
message,
...data
}));
}
export async function middleware(request) {
const { pathname } = request.nextUrl;
const startTime = Date.now();
// Public paths - سماح مباشر
if (PUBLIC_PATHS.some(path => pathname === path || pathname.startsWith(path))) {
log('Public path access', { pathname });
return NextResponse.next();
}
// Static files - سماح مباشر
if (pathname.startsWith('/_next') || pathname.startsWith('/static')) {
return NextResponse.next();
}
// تحقق من الـ Token
const token = request.cookies.get('token');
if (!token) {
log('No token - redirecting to login', { pathname });
const url = request.nextUrl.clone();
url.pathname = '/login';
url.searchParams.set('from', pathname);
return NextResponse.redirect(url);
}
try {
// Verify token
const user = await verify(token.value);
// تحقق من الصلاحيات
const allowedPaths = ROLE_ACCESS[user.role] || [];
const hasAccess = allowedPaths.some(path => pathname.startsWith(path));
if (!hasAccess) {
log('Access denied', {
userId: user.id,
role: user.role,
pathname
});
return NextResponse.redirect(new URL('/forbidden', request.url));
}
// أضف User info للـ Headers
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', user.id);
requestHeaders.set('x-user-email', user.email);
requestHeaders.set('x-user-role', user.role);
// Log success
const duration = Date.now() - startTime;
log('Request processed', {
userId: user.id,
role: user.role,
pathname,
duration: `${duration}ms`
});
return NextResponse.next({
request: { headers: requestHeaders }
});
} catch (error) {
log('Token verification failed', {
error: error.message,
pathname
});
const response = NextResponse.redirect(new URL('/login', request.url));
response.cookies.delete('token');
return response;
}
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico).*)'
]
};
🛒 مثال واقعي كامل: E-commerce App بـ App Router
عشان نفهم كل حاجة عملياً، هبني متجر إلكتروني كامل باستخدام كل الميزات اللي اتكلمنا عنها.
البنية الكاملة للمشروع
my-shop/
├── app/
│ ├── layout.js ← Root Layout
│ ├── page.js ← الصفحة الرئيسية
│ ├── (shop)/ ← Route Group للمتجر
│ │ ├── layout.js ← Shop Layout
│ │ ├── products/
│ │ │ ├── page.js ← قائمة المنتجات
│ │ │ ├── loading.js ← Loading state
│ │ │ ├── error.js ← Error handling
│ │ │ ├── [id]/
│ │ │ │ ├── page.js ← صفحة منتج
│ │ │ │ └── loading.js
│ │ │ └── actions.js ← Server Actions
│ │ ├── cart/
│ │ │ ├── page.js
│ │ │ └── actions.js
│ │ └── checkout/
│ │ ├── page.js
│ │ └── actions.js
│ ├── (auth)/ ← Route Group للـ Auth
│ │ ├── login/
│ │ │ ├── page.js
│ │ │ └── LoginForm.js
│ │ ├── register/
│ │ │ ├── page.js
│ │ │ └── RegisterForm.js
│ │ └── actions.js
│ ├── (dashboard)/ ← Route Group للـ Dashboard
│ │ ├── layout.js
│ │ ├── dashboard/
│ │ │ └── page.js
│ │ ├── orders/
│ │ │ ├── page.js
│ │ │ └── [id]/
│ │ │ └── page.js
│ │ └── profile/
│ │ ├── page.js
│ │ └── actions.js
│ └── api/
│ ├── webhook/
│ │ └── route.js ← Stripe webhook
│ └── revalidate/
│ └── route.js
├── lib/
│ ├── db.js ← Database (Prisma)
│ ├── auth.js ← Authentication helpers
│ ├── stripe.js ← Stripe integration
│ └── utils.js
├── middleware.js ← Auth middleware
└── components/
├── ProductCard.js
├── CartItem.js
└── ...
1. Database Schema (Prisma)
// prisma/schema.prisma
model User {
id String @id @default(cuid())
email String @unique
name String
password String
role String @default("user") // "user" | "admin"
orders Order[]
createdAt DateTime @default(now())
}
model Product {
id String @id @default(cuid())
name String
description String
price Float
image String
category String
stock Int
slug String @unique
orderItems OrderItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Order {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id])
total Float
status String @default("pending") // pending | paid | shipped | delivered
items OrderItem[]
createdAt DateTime @default(now())
}
model OrderItem {
id String @id @default(cuid())
orderId String
order Order @relation(fields: [orderId], references: [id])
productId String
product Product @relation(fields: [productId], references: [id])
quantity Int
price Float
}
2. Middleware للـ Authentication
// middleware.js
import { NextResponse } from 'next/server';
import { verify } from 'jsonwebtoken';
const PUBLIC_PATHS = ['/', '/products', '/login', '/register'];
const PROTECTED_PATHS = ['/dashboard', '/orders', '/profile', '/checkout'];
const ADMIN_PATHS = ['/admin'];
export async function middleware(request) {
const { pathname } = request.nextUrl;
// Public paths
if (PUBLIC_PATHS.some(path =>
pathname === path || pathname.startsWith(path + '/')
)) {
return NextResponse.next();
}
// Static files
if (pathname.startsWith('/_next') || pathname.startsWith('/api/webhook')) {
return NextResponse.next();
}
// Check authentication
const token = request.cookies.get('auth-token');
if (!token) {
const url = request.nextUrl.clone();
url.pathname = '/login';
url.searchParams.set('from', pathname);
return NextResponse.redirect(url);
}
try {
const user = verify(token.value, process.env.JWT_SECRET);
// Admin check
if (ADMIN_PATHS.some(path => pathname.startsWith(path))) {
if (user.role !== 'admin') {
return NextResponse.redirect(new URL('/forbidden', request.url));
}
}
// Add user to headers
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', user.id);
requestHeaders.set('x-user-email', user.email);
requestHeaders.set('x-user-role', user.role);
return NextResponse.next({
request: { headers: requestHeaders }
});
} catch (error) {
const response = NextResponse.redirect(new URL('/login', request.url));
response.cookies.delete('auth-token');
return response;
}
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)']
};
3. الصفحة الرئيسية - Homepage
// app/page.js
import { db } from '@/lib/db';
import { ProductCard } from '@/components/ProductCard';
import { Suspense } from 'react';
// ISR - تحديث كل 5 دقائق
export const revalidate = 300;
export const metadata = {
title: 'المتجر الإلكتروني - الرئيسية',
description: 'أفضل المنتجات بأفضل الأسعار'
};
async function FeaturedProducts() {
const products = await db.product.findMany({
where: { category: 'featured' },
take: 8,
orderBy: { createdAt: 'desc' }
});
return (
<div className="products-grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
async function Categories() {
const categories = await db.product.groupBy({
by: ['category'],
_count: { category: true }
});
return (
<div className="categories">
{categories.map(cat => (
<a key={cat.category} href={`/products?category=${cat.category}`}>
{cat.category} ({cat._count.category})
</a>
))}
</div>
);
}
export default function HomePage() {
return (
<main>
<section className="hero">
<h1>مرحباً بك في متجرنا</h1>
<p>أفضل المنتجات بأفضل الأسعار</p>
</section>
<section>
<h2>الأقسام</h2>
<Suspense fallback={<div>Loading categories...</div>}>
<Categories />
</Suspense>
</section>
<section>
<h2>المنتجات المميزة</h2>
<Suspense fallback={<div>Loading products...</div>}>
<FeaturedProducts />
</Suspense>
</section>
</main>
);
}
4. قائمة المنتجات - Products List
// app/(shop)/products/page.js
import { db } from '@/lib/db';
import { ProductCard } from '@/components/ProductCard';
import { ProductsFilter } from './ProductsFilter';
export const metadata = {
title: 'جميع المنتجات',
};
// ISR - تحديث كل 10 دقائق
export const revalidate = 600;
async function ProductsList({ searchParams }) {
const { category, sort, minPrice, maxPrice, search } = await searchParams;
const where = {};
if (category) {
where.category = category;
}
if (minPrice || maxPrice) {
where.price = {};
if (minPrice) where.price.gte = parseFloat(minPrice);
if (maxPrice) where.price.lte = parseFloat(maxPrice);
}
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ description: { contains: search, mode: 'insensitive' } }
];
}
const orderBy = sort === 'price-asc'
? { price: 'asc' }
: sort === 'price-desc'
? { price: 'desc' }
: { createdAt: 'desc' };
const products = await db.product.findMany({
where,
orderBy
});
return (
<div className="products-grid">
{products.length === 0 ? (
<p>لا توجد منتجات</p>
) : (
products.map(product => (
<ProductCard key={product.id} product={product} />
))
)}
</div>
);
}
export default function ProductsPage({ searchParams }) {
return (
<div className="products-page">
<h1>جميع المنتجات</h1>
<div className="products-layout">
<aside>
<ProductsFilter />
</aside>
<main>
<Suspense fallback={<div>جاري التحميل...</div>}>
<ProductsList searchParams={searchParams} />
</Suspense>
</main>
</div>
</div>
);
}
// app/(shop)/products/ProductsFilter.js
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import { useState } from 'react';
export function ProductsFilter() {
const router = useRouter();
const searchParams = useSearchParams();
const [search, setSearch] = useState(searchParams.get('search') || '');
const handleSearch = (e) => {
e.preventDefault();
const params = new URLSearchParams(searchParams.toString());
if (search) {
params.set('search', search);
} else {
params.delete('search');
}
router.push(`/products?${params.toString()}`);
};
const handleFilter = (key, value) => {
const params = new URLSearchParams(searchParams.toString());
if (value) {
params.set(key, value);
} else {
params.delete(key);
}
router.push(`/products?${params.toString()}`);
};
return (
<div className="filter">
<form onSubmit={handleSearch}>
<input
type="search"
placeholder="ابحث عن منتج..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<button type="submit">بحث</button>
</form>
<div>
<h3>الترتيب</h3>
<select
defaultValue={searchParams.get('sort') || ''}
onChange={(e) => handleFilter('sort', e.target.value)}
>
<option value="">الأحدث</option>
<option value="price-asc">السعر: من الأقل للأعلى</option>
<option value="price-desc">السعر: من الأعلى للأقل</option>
</select>
</div>
</div>
);
}
5. صفحة المنتج - Product Page
// app/(shop)/products/[id]/page.js
import { db } from '@/lib/db';
import { notFound } from 'next/navigation';
import { AddToCartButton } from './AddToCartButton';
import { Suspense } from 'react';
// Static Params Generation
export async function generateStaticParams() {
const products = await db.product.findMany({
select: { id: true }
});
return products.map(product => ({
id: product.id
}));
}
// Metadata
export async function generateMetadata({ params }) {
const { id } = await params;
const product = await db.product.findUnique({
where: { id }
});
if (!product) {
return {
title: 'المنتج غير موجود'
};
}
return {
title: product.name,
description: product.description,
openGraph: {
images: [product.image]
}
};
}
// ISR - تحديث كل ساعة
export const revalidate = 3600;
async function RelatedProducts({ category, currentId }) {
const products = await db.product.findMany({
where: {
category,
NOT: { id: currentId }
},
take: 4
});
return (
<div className="related-products">
<h2>منتجات مشابهة</h2>
<div className="products-grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
);
}
export default async function ProductPage({ params }) {
const { id } = await params;
const product = await db.product.findUnique({
where: { id }
});
if (!product) {
notFound();
}
return (
<div className="product-page">
<div className="product-details">
<div className="product-image">
<img src={product.image} alt={product.name} />
</div>
<div className="product-info">
<h1>{product.name}</h1>
<p className="price">{product.price} جنيه</p>
<p className="description">{product.description}</p>
<div className="stock">
{product.stock > 0 ? (
<span className="in-stock">متوفر ({product.stock} قطعة)</span>
) : (
<span className="out-of-stock">غير متوفر</span>
)}
</div>
<AddToCartButton product={product} />
</div>
</div>
<Suspense fallback={<div>جاري تحميل المنتجات المشابهة...</div>}>
<RelatedProducts
category={product.category}
currentId={product.id}
/>
</Suspense>
</div>
);
}
// app/(shop)/products/[id]/AddToCartButton.js
'use client';
import { useState } from 'react';
import { addToCart } from '../actions';
export function AddToCartButton({ product }) {
const [quantity, setQuantity] = useState(1);
const [loading, setLoading] = useState(false);
const handleAddToCart = async () => {
if (product.stock === 0) return;
setLoading(true);
await addToCart(product.id, quantity);
setLoading(false);
alert('تم إضافة المنتج للسلة!');
};
return (
<div className="add-to-cart">
<div className="quantity-selector">
<button
onClick={() => setQuantity(Math.max(1, quantity - 1))}
disabled={loading}
>
-
</button>
<span>{quantity}</span>
<button
onClick={() => setQuantity(Math.min(product.stock, quantity + 1))}
disabled={loading}
>
+
</button>
</div>
<button
onClick={handleAddToCart}
disabled={loading || product.stock === 0}
className="btn-primary"
>
{loading ? 'جاري الإضافة...' : 'أضف للسلة'}
</button>
</div>
);
}
// app/(shop)/products/actions.js
'use server';
import { cookies } from 'next/headers';
export async function addToCart(productId, quantity) {
const cookieStore = await cookies();
// Get current cart from cookies
let cart = [];
const cartCookie = cookieStore.get('cart');
if (cartCookie) {
cart = JSON.parse(cartCookie.value);
}
// Check if product already in cart
const existingItem = cart.find(item => item.productId === productId);
if (existingItem) {
existingItem.quantity += quantity;
} else {
cart.push({ productId, quantity });
}
// Save cart
cookieStore.set('cart', JSON.stringify(cart), {
maxAge: 60 * 60 * 24 * 7 // 7 days
});
return { success: true };
}
6. سلة المشتريات - Cart
// app/(shop)/cart/page.js
import { cookies } from 'next/headers';
import { db } from '@/lib/db';
import { CartItem } from './CartItem';
import Link from 'next/link';
// Dynamic - always fresh
export const dynamic = 'force-dynamic';
export const metadata = {
title: 'سلة المشتريات'
};
export default async function CartPage() {
const cookieStore = await cookies();
const cartCookie = cookieStore.get('cart');
if (!cartCookie) {
return (
<div className="empty-cart">
<h1>السلة فارغة</h1>
<Link href="/products">تصفح المنتجات</Link>
</div>
);
}
const cart = JSON.parse(cartCookie.value);
// Get products details
const productIds = cart.map(item => item.productId);
const products = await db.product.findMany({
where: { id: { in: productIds } }
});
// Combine cart with products
const cartItems = cart.map(item => ({
...item,
product: products.find(p => p.id === item.productId)
})).filter(item => item.product); // Remove invalid items
const total = cartItems.reduce(
(sum, item) => sum + (item.product.price * item.quantity),
0
);
return (
<div className="cart-page">
<h1>سلة المشتريات</h1>
<div className="cart-items">
{cartItems.map(item => (
<CartItem key={item.productId} item={item} />
))}
</div>
<div className="cart-summary">
<h2>الإجمالي: {total.toFixed(2)} جنيه</h2>
<Link href="/checkout" className="btn-primary">
إتمام الشراء
</Link>
</div>
</div>
);
}
// app/(shop)/cart/CartItem.js
'use client';
import { removeFromCart, updateQuantity } from './actions';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export function CartItem({ item }) {
const [loading, setLoading] = useState(false);
const router = useRouter();
const handleRemove = async () => {
setLoading(true);
await removeFromCart(item.productId);
router.refresh();
};
const handleUpdateQuantity = async (newQuantity) => {
if (newQuantity < 1) return;
setLoading(true);
await updateQuantity(item.productId, newQuantity);
router.refresh();
};
return (
<div className="cart-item">
<img src={item.product.image} alt={item.product.name} />
<div className="item-details">
<h3>{item.product.name}</h3>
<p className="price">{item.product.price} جنيه</p>
</div>
<div className="item-controls">
<button
onClick={() => handleUpdateQuantity(item.quantity - 1)}
disabled={loading}
>
-
</button>
<span>{item.quantity}</span>
<button
onClick={() => handleUpdateQuantity(item.quantity + 1)}
disabled={loading}
>
+
</button>
</div>
<button onClick={handleRemove} disabled={loading}>
حذف
</button>
</div>
);
}
// app/(shop)/cart/actions.js
'use server';
import { cookies } from 'next/headers';
export async function removeFromCart(productId) {
const cookieStore = await cookies();
const cartCookie = cookieStore.get('cart');
if (!cartCookie) return;
let cart = JSON.parse(cartCookie.value);
cart = cart.filter(item => item.productId !== productId);
if (cart.length === 0) {
cookieStore.delete('cart');
} else {
cookieStore.set('cart', JSON.stringify(cart));
}
}
export async function updateQuantity(productId, quantity) {
const cookieStore = await cookies();
const cartCookie = cookieStore.get('cart');
if (!cartCookie) return;
let cart = JSON.parse(cartCookie.value);
const item = cart.find(item => item.productId === productId);
if (item) {
item.quantity = quantity;
cookieStore.set('cart', JSON.stringify(cart));
}
}
7. Checkout مع Stripe
// app/(shop)/checkout/page.js
import { headers, cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { db } from '@/lib/db';
import { CheckoutForm } from './CheckoutForm';
export const dynamic = 'force-dynamic';
export const metadata = {
title: 'إتمام الطلب'
};
export default async function CheckoutPage() {
const headersList = await headers();
const userId = headersList.get('x-user-id');
if (!userId) {
redirect('/login?from=/checkout');
}
const cookieStore = await cookies();
const cartCookie = cookieStore.get('cart');
if (!cartCookie) {
redirect('/cart');
}
const cart = JSON.parse(cartCookie.value);
const productIds = cart.map(item => item.productId);
const products = await db.product.findMany({
where: { id: { in: productIds } }
});
const cartItems = cart.map(item => ({
...item,
product: products.find(p => p.id === item.productId)
})).filter(item => item.product);
const total = cartItems.reduce(
(sum, item) => sum + (item.product.price * item.quantity),
0
);
return (
<div className="checkout-page">
<h1>إتمام الطلب</h1>
<div className="checkout-layout">
<div className="order-summary">
<h2>ملخص الطلب</h2>
{cartItems.map(item => (
<div key={item.productId} className="summary-item">
<span>{item.product.name} x {item.quantity}</span>
<span>{(item.product.price * item.quantity).toFixed(2)} جنيه</span>
</div>
))}
<div className="total">
<strong>الإجمالي:</strong>
<strong>{total.toFixed(2)} جنيه</strong>
</div>
</div>
<CheckoutForm total={total} items={cartItems} />
</div>
</div>
);
}
// app/(shop)/checkout/CheckoutForm.js
'use client';
import { useFormState, useFormStatus } from 'react-dom';
import { createOrder } from './actions';
import { loadStripe } from '@stripe/stripe-js';
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending} className="btn-primary">
{pending ? 'جاري المعالجة...' : 'إتمام الدفع'}
</button>
);
}
export function CheckoutForm({ total, items }) {
const [state, formAction] = useFormState(createOrder, null);
return (
<form action={formAction} className="checkout-form">
{state?.error && (
<div className="error">{state.error}</div>
)}
<div className="form-group">
<label htmlFor="address">العنوان</label>
<textarea
id="address"
name="address"
required
rows={3}
/>
</div>
<div className="form-group">
<label htmlFor="phone">رقم الهاتف</label>
<input
id="phone"
name="phone"
type="tel"
required
/>
</div>
<SubmitButton />
</form>
);
}
// app/(shop)/checkout/actions.js
'use server';
import { db } from '@/lib/db';
import { headers, cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export async function createOrder(prevState, formData) {
const headersList = await headers();
const userId = headersList.get('x-user-id');
if (!userId) {
return { error: 'يجب تسجيل الدخول' };
}
const address = formData.get('address');
const phone = formData.get('phone');
// Get cart
const cookieStore = await cookies();
const cartCookie = cookieStore.get('cart');
if (!cartCookie) {
return { error: 'السلة فارغة' };
}
const cart = JSON.parse(cartCookie.value);
// Get products
const productIds = cart.map(item => item.productId);
const products = await db.product.findMany({
where: { id: { in: productIds } }
});
// Calculate total
const total = cart.reduce((sum, item) => {
const product = products.find(p => p.id === item.productId);
return sum + (product.price * item.quantity);
}, 0);
try {
// Create order
const order = await db.order.create({
data: {
userId,
total,
status: 'pending',
items: {
create: cart.map(item => {
const product = products.find(p => p.id === item.productId);
return {
productId: item.productId,
quantity: item.quantity,
price: product.price
};
})
}
}
});
// Create Stripe Checkout Session
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: cart.map(item => {
const product = products.find(p => p.id === item.productId);
return {
price_data: {
currency: 'egp',
product_data: {
name: product.name,
images: [product.image]
},
unit_amount: Math.round(product.price * 100)
},
quantity: item.quantity
};
}),
mode: 'payment',
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/orders/${order.id}?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/checkout?canceled=true`,
metadata: {
orderId: order.id
}
});
// Clear cart
cookieStore.delete('cart');
// Redirect to Stripe
redirect(session.url);
} catch (error) {
console.error('Checkout error:', error);
return { error: 'حدث خطأ أثناء إتمام الطلب' };
}
}
8. Stripe Webhook
// app/api/webhook/route.js
import { db } from '@/lib/db';
import { headers } from 'next/headers';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export async function POST(request) {
const body = await request.text();
const signature = headers().get('stripe-signature');
let event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return new Response('Webhook Error', { status: 400 });
}
// Handle the event
switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object;
// Update order status
await db.order.update({
where: { id: session.metadata.orderId },
data: { status: 'paid' }
});
// Update product stock
const order = await db.order.findUnique({
where: { id: session.metadata.orderId },
include: { items: true }
});
for (const item of order.items) {
await db.product.update({
where: { id: item.productId },
data: { stock: { decrement: item.quantity } }
});
}
break;
default:
console.log(`Unhandled event type ${event.type}`);
}
return new Response(JSON.stringify({ received: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
هذا مثال كامل ومتكامل! يوضح:
- ✅ Server Components للـ Data Fetching
- ✅ Client Components للـ Interactivity
- ✅ Server Actions للـ Mutations
- ✅ Middleware للـ Authentication
- ✅ ISR و Dynamic Rendering
- ✅ Suspense و Streaming
- ✅ Metadata API
- ✅ Route Groups
- ✅ تكامل مع Stripe
- ✅ Webhooks
