13707 words
69 minutes
App Router vs Pages Router

🔥 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 بتقول: طب ليه نبعت كل الكود للعميل؟ خلينا نقسم المكونات لنوعين:

  1. Server Components - بتشتغل على السيرفر بس، مش بتروح للعميل خالص
  2. 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 ComponentClient 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.jsAPI 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>
  );
}

إزاي بتشتغل:

  1. وقت الـ Build: Next.js بتنفذ المكون وتولد HTML
  2. لما يجي request: بتبعت الـ HTML الجاهز فوراً (أسرع حاجة ممكنة!)
  3. مفيش سيرفر 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;

إزاي بتشتغل:

  1. Request يجي للسيرفر
  2. السيرفر ينفذ المكون ويجيب البيانات
  3. يولد HTML ويبعته للعميل
  4. كل 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
ISRBuild + خلفية⚡⚡ سريع جداًمحتوى بيتحدث بفتراتمؤقتgetStaticProps + revalidate
StreamingRequest تدريجي⚡⚡ جزئي سريعصفحات معقدةحسب الجزء❌ مفيش

إزاي تختار النوع المناسب؟#

سؤال نفسك:

  1. البيانات بتتغير قد إيه؟

    • مش بتتغير → Static (SSG)
    • بتتغير كل شوية → ISR
    • بتتغير مع كل request → Dynamic (SSR)
  2. الصفحة فيها أجزاء بطيئة؟

    • آه → Streaming مع Suspense
    • لأ → شوف السؤال الأول
  3. محتاج 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 RouterApp 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;

اللي بيحصل:

  1. أول Build: Next.js بتنفذ المكون وتولد HTML
  2. Requests التالية: بتبعت HTML الجاهز فوراً (أسرع حاجة!)
  3. بعد 10 دقائق: بتحدث الـ HTML في الخلفية
  4. 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 Routesdynamic = 'force-dynamic'
Router Cacheالمتصفح5 دقائق (Static) / 30 ثانية (Dynamic)Navigationrouter.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 RouterApp Router
نوع المكوناتكلها Client افتراضيًاServer افتراضيًا
جلب البياناتgetStaticProps, getServerSidePropsfetch() جوه المكون
التخطيطات_app.js, _document.jslayout.js متداخل
Loading Statesيدويloading.js تلقائي
Error Handlingيدويerror.js تلقائي
الأداءجيدممتاز
حجم الباندلكبير نسبيًاأصغر بكتير
التوجيهnext/routernext/navigation
SEOnext/headMetadata API
الكاشمحدودمرن جدًا
Streaming
Parallel Routes❌ (صعب)✅ مدمج
Intercepting Routes
API Routespages/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 (استخدم env variables مباشرة)
  • 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

Join our whatsapp group here
My Channel here