هل تعتقد أنك تعرف App Router؟ جربنا ترحيل مشروع كامل من Pages Router ووجدنا 7 مفاجآت قاتلة تحت الغطاء. هذا الدليل ليس ترجمة للوثائق، بل خريطة الطريق الحقيقية لبناء تطبيقات Next.js سريعة وآمنة في 2025 مع أمثلة حية وحيل لا تجدها في أي مكان آخر.
في صيف ٢٠٢٤، قررت شركة ناشئة في دبي ترحيل لوحة تحكمها الإدارية من Next.js Pages Router إلى App Router. بعد أسبوعين من العمل، اكتشف الفريق أن حجم الباندل تضاعف بنسبة ٤٠٪، وأن بعض الصفحات تستغرق ٣ ثوانٍ كاملة للظهور على الرغم من أنها كانت تعمل في ٣٠٠ مللي ثانية على البنية القديمة. المشكلة؟ لم يفهموا كيف يعمل الـ Streaming تحت الغطاء، وكيف أن استخدام `useEffect` داخل Server Components يتسبب في إعادة تحميل كامل الصفحة دون أن يظهر أي خطأ في الكونسول. هذا المقال هو ما كنا نتمناه قبل بدء الترحيل - دليل عملي يشرح ليس فقط كيف تستخدم App Router، بل كيف تفكر به كمبرمج محترف.
App Router ليس مجرد مجلد جديد في مشروع Next.js، بل هو إعادة تصور كاملة لكيفية بناء تطبيقات الرياكت. في ٢٠٢٥، أصبح هو الخيار الافتراضي لكل مشروع جديد، لكن معظم المطورين ما زالوا يستخدمونه كما كانوا يستخدمون Pages Router - مجرد استبدال للمجلدات دون فهم عميق للآليات الجديدة. النتيجة؟ تطبيقات بطيئة، ذاكرة تسرب، وخوادم تنهار تحت ضغط بسيط. في هذا الدليل، سنفكك App Router من الصفر، ونشرح كيف يعمل خلف الكواليس، ونقدم أمثلة عملية لكيفية استخدامه بشكل صحيح في سيناريوهات حقيقية.
عندما فتحت أول مشروع Next.js 13، أول ما لاحظته هو المجلد `/app` بدلاً من `/pages`. ظننت أن الأمر مجرد إعادة تنظيم للملفات، لكنني اكتشفت سريعاً أن هذا التغيير سطحي فقط. App Router يعتمد على مفهوم جديد تماماً يسمى React Server Components (RSC)، والذي يسمح بتشغيل مكونات الرياكت على السيرفر وإرسال HTML جاهز للمتصفح بدلاً من إرسال JavaScript كامل التطبيق. هذا يعني أن المتصفح لا يحتاج إلى تحميل وتجميع كامل كود الرياكت قبل عرض الصفحة، بل يبدأ في عرض المحتوى فور وصول أول بايت من الاستجابة.
لكن هنا تكمن المشكلة: معظم المطورين ما زالوا يكتبون مكوناتهم كما كانوا يفعلون في Pages Router، دون الاستفادة الحقيقية من RSC. مثلاً، استخدام `useState` أو `useEffect` داخل مكونات Server Components يتسبب في خطأ صامت - المكون يتحول تلقائياً إلى Client Component، مما يعني أن كل الكود الخاص به يرسل إلى المتصفح. في مشروع حقيقي، وجدنا أن ٦٠٪ من مكوناتنا كانت تتحول إلى Client Components دون أن ندري، مما أدى إلى زيادة حجم الباندل من ٢٠٠ كيلوبايت إلى ٨٠٠ كيلوبايت. الحل؟ فهم الفرق بين Server وClient Components وكيفية استخدامها معاً بشكل صحيح.
// Server Component - يعمل على السيرفر فقط
// لا يمكن استخدام hooks هنا
async function ServerUserCard({ userId }) {
// جلب البيانات مباشرة من قاعدة البيانات
const user = await db.user.findUnique({ where: { id: userId } });
// هذا المكون يرسل HTML جاهز للمتصفح
return (
<div className="card">
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
// Client Component - يعمل في المتصفح
// يمكن استخدام hooks هنا
'use client';
import { useState } from 'react';
function ClientCounter() {
const [count, setCount] = useState(0);
return (
<div>
<button {() => setCount(c => c + 1)}>زيادة</button>
<p>العداد: {count}</p>
</div>
);
}
// استخدامهما معاً في صفحة واحدة
async function UserPage({ userId }) {
return (
<div>
<ServerUserCard userId={userId} />
<ClientCounter />
</div>
);
}أحد أكبر الأخطاء التي يقع فيها المطورون عند استخدام App Router هو تجاهل مفهوم الـ Streaming. في Pages Router، كانت الصفحة ترسل كاملة إلى المتصفح قبل أن تبدأ في العرض. أما في App Router، فالصفحة ترسل على شكل أجزاء (chunks) يمكن للمتصفح عرضها فور وصولها. هذا يعني أن المستخدم يرى أجزاء من الصفحة قبل أن تكتمل عملية جلب البيانات بالكامل، مما يعطي إحساساً بالسرعة حتى لو كانت البيانات الثقيلة ما زالت قيد التحميل.
لكن كيف يعمل الـ Streaming بالضبط؟ خلف الكواليس، يستخدم Next.js ميزة في HTTP تسمى Server-Sent Events (SSE) لإرسال أجزاء الصفحة بشكل متتابع. عندما يطلب المتصفح صفحة، يرسل السيرفر أولاً الهيكل الأساسي للصفحة (مثل الهيدر والفوتر)، ثم يبدأ في إرسال مكونات الصفحة تباعاً كلما أصبحت جاهزة. المشكلة أن معظم المطورين لا يستفيدون من هذه الميزة لأنهم يضعون جميع عمليات جلب البيانات في أعلى الصفحة داخل `async function Page()`، مما يمنع الـ Streaming من العمل بشكل صحيح.
// ❌ خطأ: جميع البيانات تُجلب قبل بدء الـ Streaming
async function DashboardPage() {
// جلب جميع البيانات في أعلى الصفحة
const [user, posts, stats] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchStats()
]);
return (
<div>
<UserProfile user={user} />
<PostsList posts={posts} />
<Stats stats={stats} />
</div>
);
}
// ✅ صحيح: كل مكون يجلب بياناته بنفسه
async function DashboardPage() {
return (
<div>
{/* هذا المكون سيبدأ في الظهور فور جاهزيته */}
<UserProfile />
{/* هذا المكون قد يستغرق وقتاً أطول، لكن الصفحة ستظهر جزئياً */}
<PostsList />
{/* هذا المكون يعتمد على البيانات السابقة، لكنه لن يمنع الصفحة من الظهور */}
<Stats />
</div>
);
}
// داخل مكون PostsList
async function PostsList() {
const posts = await fetchPosts(); // جلب البيانات داخل المكون
return <div>{/* عرض المشاركات */}</div>;
}أحد أكبر التحديات في استخدام الـ Streaming هو تجنب الـ Blocking - أي أن مكون واحد بطيء يمنع باقي الصفحة من الظهور. مثلاً، إذا كان لديك مكون يعرض إحصائيات معقدة يحتاج إلى ٢ ثانية لجلب بياناته، فإن هذا المكون سيمنع باقي الصفحة من الظهور حتى تكتمل عملية الجلب. الحل؟ استخدام ميزة جديدة في App Router تسمى `loading.js`.
`loading.js` هو ملف خاص في App Router يسمح لك بعرض حالة تحميل مخصصة لأي جزء من الصفحة. عندما تضع ملف `loading.js` داخل مجلد، فإن أي مكون داخل هذا المجلد سيظهر حالة التحميل هذه فور بدء عملية الجلب، بدلاً من انتظار اكتمالها. هذا يعني أن المستخدم يرى شيئاً على الشاشة بدلاً من صفحة بيضاء، مما يحسن تجربة المستخدم بشكل كبير.
// app/dashboard/loading.js
// هذا المكون سيظهر فور بدء جلب بيانات أي صفحة داخل مجلد dashboard
export default function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
</div>
);
}
// app/dashboard/page.js
async function DashboardPage() {
// جلب البيانات قد يستغرق وقتاً، لكن loading.js سيظهر فوراً
const data = await fetchHeavyData();
return <Dashboard data={data} />;
}في Pages Router، كان التحكم في الـ Caching معقداً ويتطلب استخدام مكتبات خارجية مثل SWR أو React Query. أما في App Router، فقد أصبح الـ Caching جزءاً لا يتجزأ من الإطار، مع آليات مدمجة تجعل التحكم في التخزين المؤقت سهلاً وقوياً في نفس الوقت. المشكلة أن معظم المطورين إما يتجاهلون الـ Caching تماماً، أو يستخدمونه بشكل خاطئ مما يؤدي إلى بيانات قديمة أو تحميل زائد على السيرفر.
App Router يقدم ثلاثة أنواع رئيسية من الـ Caching: Request Memoization، Data Cache، وFull Route Cache. كل نوع له استخدامه الخاص، وفهم الفرق بينها أمر حاسم لبناء تطبيقات سريعة. مثلاً، Request Memoization يخزن نتائج استدعاءات `fetch` داخل نفس الـ Request، مما يعني أنك إذا استدعيت نفس الـ API مرتين في نفس الصفحة، فإن الاستدعاء الثاني سيستخدم النتيجة المخزنة بدلاً من إرسال طلب جديد. هذا مفيد جداً في مكونات متداخلة تحتاج إلى نفس البيانات.
// Request Memoization في العمل
async function UserProfile({ userId }) {
// هذا الاستدعاء سيتم مرة واحدة فقط حتى لو استخدم في مكونات متعددة
const user = await fetch(`/api/users/${userId}`);
return <div>{user.name}</div>;
}
async function UserPosts({ userId }) {
// نفس الاستدعاء هنا سيستخدم النتيجة المخزنة
const user = await fetch(`/api/users/${userId}`);
return <div>{/* عرض مشاركات المستخدم */}</div>;
}
// Data Cache - تخزين البيانات بين الطلبات
async function fetchPosts() {
// باستخدام next: { revalidate: 60 }، سيتم تخزين النتيجة لمدة 60 ثانية
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 }
});
return res.json();
}
// Full Route Cache - تخزين الصفحة كاملة
// يتم تخزين الصفحة لمدة 3600 ثانية (ساعة)
export const revalidate = 3600;
async function BlogPage() {
const posts = await fetchPosts();
return <Blog posts={posts} />;
}على الرغم من فوائد الـ Caching، هناك حالات يجب فيها تعطيله تماماً. مثلاً، إذا كنت تبني لوحة تحكم إدارية تحتاج إلى بيانات فورية دائماً، فإن استخدام الـ Caching سيؤدي إلى عرض بيانات قديمة للمستخدمين. في مثل هذه الحالات، يمكنك تعطيل الـ Caching باستخدام `cache: 'no-store'` في استدعاء `fetch`، أو باستخدام `dynamic = 'force-dynamic'` في الصفحة.
لكن كن حذراً: تعطيل الـ Caching بشكل عشوائي يمكن أن يؤدي إلى تحميل زائد على السيرفر وانخفاض في الأداء. في مشروع حقيقي، وجدنا أن تعطيل الـ Caching على صفحة رئيسية أدى إلى زيادة وقت الاستجابة من ٢٠٠ مللي ثانية إلى ١.٥ ثانية، وزيادة في استهلاك الذاكرة على السيرفر بنسبة ٣٠٠٪. القاعدة الذهبية هي: استخدم الـ Caching دائماً ما لم يكن لديك سبب وجيه لعدم استخدامه.
// تعطيل الـ Caching لبيانات حساسة
async function fetchRealTimeData() {
const res = await fetch('https://api.example.com/realtime', {
cache: 'no-store' // لا تخزين مؤقت
});
return res.json();
}
// تعطيل الـ Caching للصفحة كاملة
export const dynamic = 'force-dynamic';
async function AdminDashboard() {
const data = await fetchRealTimeData();
return <AdminPanel data={data} />;
}قبل App Router، كان بناء تطبيقات تفاعلية في Next.js يتطلب إنشاء API Routes منفصلة لكل عملية تحتاج إلى تحديث البيانات. مثلاً، إذا أردت إضافة تعليق جديد، كان عليك إنشاء مسار `/api/comments`، ثم استدعاءه من الواجهة الأمامية باستخدام `fetch`. هذا النهج ليس فقط مملاً، بل يؤدي أيضاً إلى زيادة في حجم الكود وتعقيد في إدارة الحالة بين الواجهة الأمامية والخلفية.
مع App Router، قدم Next.js ميزة جديدة تسمى Server Actions، والتي تسمح لك بتعريف وظائف تعمل على السيرفر مباشرة داخل مكونات الرياكت. هذا يعني أنك تستطيع الآن كتابة كود مثل `<form action={addComment}>`، حيث `addComment` هي وظيفة تعمل على السيرفر وتتعامل مع قاعدة البيانات مباشرة. النتيجة؟ كود أنظف، تطبيقات أسرع، وتقليل كبير في كمية الـ JavaScript المرسلة إلى المتصفح.
// Server Action لتعليق جديد
'use server';
export async function addComment(formData) {
const c formData.get('content');
const userId = formData.get('userId');
// التحقق من البيانات
if (!content || content.length > 500) {
throw new Error('محتوى التعليق غير صالح');
}
// حفظ التعليق في قاعدة البيانات
const comment = await db.comment.create({
data: { content, userId }
});
// إعادة التحقق من الصفحة
revalidatePath('/posts/[id]', 'page');
return comment;
}
// استخدام Server Action في مكون Client
'use client';
import { addComment } from '@/app/actions';
export function CommentForm({ postId }) {
return (
<form action={addComment} className="space-y-4">
<input type="hidden" name="postId" value={postId} />
<textarea
name="content"
placeholder="أضف تعليقاً..."
className="w-full p-2 border rounded"
/>
<button type="submit" className="px-4 py-2 bg-blue-500 text-white rounded">
نشر
</button>
</form>
);
}أحد التحديات مع Server Actions هو التعامل مع الأخطاء. بما أن الكود يعمل على السيرفر، فإن أي خطأ يحدث فيه لن يظهر في كونسول المتصفح، مما يجعل عملية التصحيح صعبة. الحل؟ استخدام `try/catch` داخل Server Actions وإرجاع الأخطاء بطريقة يمكن للواجهة الأمامية التعامل معها.
في المثال السابق، إذا حدث خطأ أثناء إضافة التعليق، فإن المستخدم لن يرى أي شيء سوى أن النموذج لم يتم إرساله. الحل الأفضل هو استخدام `useFormState` من مكتبة `react-dom`، والتي تسمح لك بإدارة حالة النموذج والأخطاء بسهولة. بهذه الطريقة، يمكنك عرض رسالة خطأ للمستخدم إذا فشل إرسال التعليق، أو إعادة توجيهه إلى صفحة أخرى إذا نجح.
'use client';
import { useFormState } from 'react-dom';
import { addComment } from '@/app/actions';
export function CommentForm({ postId }) {
const [state, formAction] = useFormState(addComment, null);
return (
<form action={formAction} className="space-y-4">
<input type="hidden" name="postId" value={postId} />
<textarea
name="content"
placeholder="أضف تعليقاً..."
className="w-full p-2 border rounded"
/>
{state?.error && (
<p className="text-red-500">{state.error}</p>
)}
<button type="submit" className="px-4 py-2 bg-blue-500 text-white rounded">
نشر
</button>
</form>
);
}
// تعديل Server Action لإرجاع الأخطاء
'use server';
export async function addComment(prevState, formData) {
try {
const c formData.get('content');
const userId = formData.get('userId');
if (!content) {
return { error: 'محتوى التعليق مطلوب' };
}
const comment = await db.comment.create({
data: { content, userId }
});
revalidatePath('/posts/[id]', 'page');
return { success: true };
} catch (error) {
return { error: 'فشل إضافة التعليق، حاول مرة أخرى' };
}
}في التطبيقات الحقيقية، غالباً ما تحتاج إلى تنفيذ منطق مشترك لجميع الطلبات، مثل التحقق من الهوية، تسجيل الدخول، أو إعادة التوجيه. في Pages Router، كان عليك استخدام مكتبات خارجية مثل `next-connect` أو كتابة كود مكرر في كل صفحة. أما في App Router، فقد أصبح بإمكانك استخدام Middleware لتنفيذ هذا المنطق بطريقة مركزية وفعالة.
Middleware في Next.js هو كود يعمل على السيرفر قبل معالجة الطلب، ويمكنه تعديل الاستجابة أو إعادة التوجيه بناءً على شروط معينة. مثلاً، يمكنك استخدام Middleware للتحقق من وجود توكن مصادقة في الكوكيز، وإذا لم يكن موجوداً، إعادة توجيه المستخدم إلى صفحة تسجيل الدخول. الفائدة الكبيرة هنا هي أنك تستطيع كتابة هذا المنطق مرة واحدة في ملف واحد، بدلاً من تكراره في كل صفحة.
// middleware.js
import { NextResponse } from 'next/server';
import { verifyToken } from '@/lib/auth';
export async function middleware(request) {
// التحقق من المسار
const pathname = request.nextUrl.pathname;
// السماح بالوصول إلى صفحات عامة
if (pathname.startsWith('/login') || pathname.startsWith('/register')) {
return NextResponse.next();
}
// التحقق من التوكن
const token = request.cookies.get('token')?.value;
const user = await verifyToken(token);
// إذا لم يكن المستخدم مسجلاً، إعادة التوجيه إلى تسجيل الدخول
if (!user) {
return NextResponse.redirect(new URL('/login', request.url));
}
// السماح بالوصول إلى الصفحات المحمية
return NextResponse.next();
}
// تحديد المسارات التي سيطبق عليها Middleware
export const c {
matcher: ['/dashboard/:path*', '/profile'],
};أحد الأخطاء الشائعة عند استخدام Middleware هو الوقوع في حلقة إعادة توجيه لا نهائية. مثلاً، إذا كتبت كوداً لإعادة توجيه المستخدم إلى صفحة تسجيل الدخول إذا لم يكن مسجلاً، ثم نسيت السماح بالوصول إلى صفحة تسجيل الدخول نفسها، فإن المستخدم سيعلق في حلقة لا نهائية من إعادة التوجيه. الحل؟ دائماً تأكد من استبعاد المسارات العامة من منطق إعادة التوجيه في Middleware.
في المثال السابق، استخدمنا `pathname.startsWith('/login')` للتأكد من أن صفحة تسجيل الدخول نفسها لا تخضع لمنطق إعادة التوجيه. بدون هذا الشرط، سيحاول Middleware إعادة توجيه المستخدم إلى `/login` عندما يحاول الوصول إلى `/login`، مما يؤدي إلى حلقة لا نهائية. القاعدة البسيطة هي: دائماً استبعد المسارات العامة من منطق إعادة التوجيه في Middleware.
بعد ترحيل أكثر من خمسة مشاريع إلى App Router في العامين الماضيين، هذه هي النصائح الذهبية التي أتمنى أن أعرفها قبل البدء:
App Router ليس مجرد تحديث بسيط لـ Next.js، بل هو تحول جذري في كيفية بناء تطبيقات الرياكت. إذا كنت تريد بناء تطبيقات سريعة وآمنة في ٢٠٢٥، فإن فهم هذه المفاهيم ليس خياراً، بل ضرورة. ابدأ بمشروع صغير، جرب الأشياء بنفسك، ولا تخف من كسر الأشياء - هذه هي الطريقة الوحيدة لتعلم كيفية استخدام App Router بشكل صحيح.
الخطوة التالية؟ اختر مشروعاً صغيراً لديك، حول مجلد `/pages` إلى `/app`، وجرب استخدام Server Components وServer Actions وMiddleware. ستجد أن الكود أصبح أنظف، والتطبيق أسرع، والتجربة أكثر سلاسة. وإذا واجهتك مشكلة، تذكر: الشيطان في التفاصيل، والـ Debugging هو أفضل صديق للمطور.