هل تعاني من تعقيد الـ Routing في Next.js؟ اكتشف كيف يحل App Router مشاكلك في 2025 بأمثلة عملية تكشف ما تخفيه الوثائق الرسمية عنك، من الـ Server Components إلى الـ Streaming.
في صيف ٢٠٢٤، قضيت ثلاثة أيام كاملة أحاول فهم لماذا يتجمد تطبيق Next.js الخاص بي عند تحميل صفحة المستخدمين. المشكلة لم تكن في الكود نفسه، بل في طريقة تعامل App Router مع الـ Data Fetching داخل الـ Server Components. بعد مراجعة ٤٧ issue على GitHub وdebugging عميق باستخدام Chrome DevTools، اكتشفت أن الـ Suspense Boundary الذي وضعته كان يعطل الـ Streaming تماماً لأنني استخدمت await داخل مكون لم يكن بحاجة إليه. هذه ليست مجرد مشكلة تقنية، بل هي فجوة معرفية كبيرة في مجتمع React حول كيفية عمل App Router خلف الكواليس.
App Router ليس مجرد تحديث بسيط للـ Pages Router القديم، بل هو إعادة تفكير كاملة في كيفية بناء تطبيقات React على الويب. في ٢٠٢٥، أصبح هو الخيار الافتراضي في Next.js، ومع ذلك لا يزال الكثير من المطورين يستخدمونه بطريقة خاطئة، إما بتجاهل الـ Server Components تماماً أو بإساءة استخدامها لدرجة أن التطبيق يصبح أبطأ من نسخة Pages Router. في هذا الدليل، سأريك كيف يعمل App Router حقاً، من الـ File-system Routing إلى الـ Streaming، مع أمثلة عملية تكشف التفاصيل التي تغفل عنها معظم الشروحات السطحية.
عندما تنشئ مجلداً باسم app داخل مشروع Next.js، فإنك لا تنشئ مجرد نظام routing تقليدي، بل تنشئ ما يمكن تسميته بـ "شجرة تنفيذ ديناميكية". كل مجلد داخل app يمثل مساراً URL، ولكن ما يحدث خلف الكواليس يختلف تماماً عن الـ Pages Router. على سبيل المثال، ملف page.tsx داخل مجلد app/dashboard/users لا يولد مجرد مكون React يتم تحميله من قبل المتصفح، بل يولد مكوناً يتم تنفيذه جزئياً على السيرفر وجزئياً على العميل، اعتماداً على كيفية تعريفه.
المفاجأة الكبرى هنا هي أن Next.js يقوم بتحليل شجرتك المكونية في وقت البناء (build time) ويحدد أي المكونات يمكن تنفيذها على السيرفر فقط (Server Components) وأيها يجب أن يتم تنفيذه على العميل (Client Components). هذه العملية ليست مجرد تحليل سطحي، بل تعتمد على عدة عوامل مثل استخدام hooks معينة (useState, useEffect) أو استيراد مكتبات تعتمد على الـ DOM. إذا استخدمت useState داخل مكون لم تحدد أنه Client Component، فستحصل على خطأ في وقت البناء، وليس في وقت التشغيل، وهذا يعني أن Next.js يقوم بفحص شفرتك بشكل عميق قبل حتى أن تصل إلى المتصفح.
// مثال على مكون Server Component لا يمكن تنفيذه على العميل
// لاحظ عدم وجود أي hooks أو مكتبات تعتمد على DOM
async function UserList({ users }: { users: User[] }) {
// هذا الكود يتم تنفيذه بالكامل على السيرفر
// حتى لو استخدمت await هنا، فهذا لا يعني أن المكون Client Component
const sortedUsers = await getSortedUsers(users);
return (
<ul>
{sortedUsers.map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
);
}
// هذا المكون Client Component لأنه يستخدم useState
'use client';
function UserFilter() {
const [filter, setFilter] = useState('');
return (
<input
type="text"
value={filter}
{(e) => setFilter(e.target.value)}
/>
);
}الوهم الشائع هو أن الـ Server Components وClient Components يعملان بشكل مستقل، لكن الحقيقة هي أن Next.js يقوم بعملية تسمى "serialization" عند تمرير البيانات بينهما. عندما تمرر بيانات من Server Component إلى Client Component، فإن Next.js يحول هذه البيانات إلى JSON يمكن قراءته من قبل العميل. هذا يعني أنك لا تستطيع تمرير كائنات معقدة مثل دوال أو مكونات React نفسها، بل فقط البيانات البسيطة مثل الأرقام والنصوص والمصفوفات والكائنات البسيطة.
المشكلة الأكبر هنا هي أن الكثير من المطورين يحاولون تمرير مكونات كاملة من Server Component إلى Client Component، معتقدين أن هذا سيحسن الأداء. في الواقع، هذا يؤدي إلى أخطاء في وقت البناء أو حتى إلى تسرب ذاكرة (Memory Leak) لأن Next.js يحاول تسلسل مكونات ليست قابلة للتسلسل. الحل هو تصميم شجرتك المكونية بحيث تكون الـ Client Components في أسفل الشجرة، وتتلقى البيانات فقط من الـ Server Components عبر props، وليس العكس.
// ❌ خطأ شائع: محاولة تمرير مكون من Server Component إلى Client Component
async function DashboardLayout({ children }: { children: React.ReactNode }) {
const user = await getCurrentUser();
return (
<div>
{/* هذا سيؤدي إلى خطأ في وقت البناء */}
<UserProfile user={user} />
{children}
</div>
);
}
// ✅ الحل الصحيح: تمرير البيانات فقط، وليس المكونات
'use client';
function UserProfile({ user }: { user: User }) {
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}في Pages Router، كنا نستخدم getServerSideProps وgetStaticProps للحصول على البيانات. في App Router، تغير كل شيء مع ظهور Server Components وServer Actions. المشكلة هي أن الكثير من المطورين يستخدمون هذه الأدوات بشكل عشوائي دون فهم متى يجب استخدام كل منها. مثلاً، إذا كنت تريد تحديث بيانات المستخدم بعد إرسال نموذج، فهل تستخدم Server Action أم Route Handler؟ الجواب يعتمد على عدة عوامل، أهمها ما إذا كنت تريد إعادة تحميل الصفحة أم لا.
Server Actions هي الطريقة الجديدة لإرسال البيانات من العميل إلى السيرفر دون الحاجة إلى كتابة API routes منفصلة. يمكنك تعريفها مباشرة داخل مكون Server Component باستخدام الدالة 'use server'، وهي تعمل بشكل مشابه للـ Form Actions في HTML التقليدي، لكنها تدعم الـ TypeScript وReact بشكل كامل. لكن هنا تكمن المشكلة: Server Actions ليست مناسبة لجميع الحالات. إذا كنت تريد تنفيذ عملية طويلة الأمد مثل إرسال بريد إلكتروني أو معالجة ملف كبير، فإن استخدام Server Action قد يؤدي إلى تجميد واجهة المستخدم لأن العملية تتم بشكل متزامن مع الـ Event Loop الخاص بالعميل.
// مثال على Server Action داخل مكون Server Component
async function UpdateUserForm({ user }: { user: User }) {
async function updateUser(formData: FormData) {
'use server';
const name = formData.get('name') as string;
await db.user.update({ where: { id: user.id }, data: { name } });
revalidatePath('/dashboard'); // إعادة تحميل البيانات دون إعادة تحميل الصفحة
}
return (
<form action={updateUser}>
<input type="text" name="name" defaultValue={user.name} />
<button type="submit">تحديث</button>
</form>
);
}Route Handlers في App Router هي البديل لـ API Routes في Pages Router، لكنها أكثر قوة ومرونة. استخدم Route Handlers عندما تحتاج إلى تنفيذ عمليات I/O Bound مثل تحميل الملفات أو الاتصال بخدمات خارجية مثل Stripe أو SendGrid. السبب هو أن Route Handlers تعمل بشكل مستقل عن دورة حياة المكون، ويمكنك التحكم الكامل في الـ Response الذي ترسله، بما في ذلك الـ Streaming Responses أو الـ Server-Sent Events.
في أحد المشاريع التي عملت عليها، كنا نستخدم Server Actions لتحديث بيانات المستخدم، لكن واجهنا مشكلة عندما حاولنا إرسال ملفات كبيرة. العملية كانت تستغرق أكثر من ٣٠ ثانية، مما يؤدي إلى timeout في الـ Server Action. الحل كان استخدام Route Handler مع تحميل الملفات بشكل متزامن باستخدام FormData، مما سمح لنا بعرض شريط تقدم للمستخدم وإرسال البيانات بشكل أكثر كفاءة. هذا يوضح أن Server Actions ليست الحل السحري لكل شيء، بل هي أداة أخرى في صندوق أدواتك يجب استخدامها بحكمة.
// Route Handler للتعامل مع تحميل الملفات
// ملف: app/api/upload/route.ts
export async function POST(request: Request) {
const formData = await request.formData();
const file = formData.get('file') as File;
if (!file) {
return new Response(JSON.stringify({ error: 'No file uploaded' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
// معالجة الملف باستخدام مكتبة مثل busboy أو formidable
const buffer = await file.arrayBuffer();
// ... تحميل الملف إلى S3 أو خدمة تخزين أخرى
return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}واحدة من أقوى ميزات App Router هي القدرة على استخدام Streaming لعرض المحتوى للمستخدم بشكل تدريجي. الفكرة بسيطة: بدلاً من انتظار تحميل جميع البيانات قبل عرض الصفحة، يمكنك عرض أجزاء من الصفحة فور توفرها، مما يجعل التطبيق يشعر بأنه أسرع بكثير. لكن هنا تكمن المشكلة: الكثير من المطورين يستخدمون Streaming بطريقة خاطئة، إما بوضع جميع المكونات داخل Suspense Boundary واحد، أو باستخدام await داخل مكونات لا تحتاج إليها، مما يعطل عملية Streaming تماماً.
المفتاح لفهم Streaming في App Router هو معرفة أن كل Suspense Boundary يولد ما يسمى بـ "streaming chunk". عندما تضع مكوناً داخل Suspense، فإن Next.js يرسل هذا المكون إلى المتصفح بمجرد توفر بياناته، دون انتظار بقية الصفحة. لكن إذا استخدمت await داخل مكون غير معلّم بـ Suspense، فإن Next.js ينتظر اكتمال هذا المكون قبل إرسال أي شيء إلى المتصفح، مما يهزم الغرض من Streaming تماماً. في أحد المشاريع، قمنا بتقليل وقت التحميل المدرك من ٤ ثوانٍ إلى أقل من ثانية واحدة فقط عن طريق إعادة هيكلة الـ Suspense Boundaries بشكل صحيح.
// ❌ خطأ شائع: استخدام await داخل مكون غير معلّم بـ Suspense
async function DashboardPage() {
// هذا await يعطل Streaming بالكامل
const user = await getCurrentUser();
const posts = await getRecentPosts();
return (
<div>
<UserProfile user={user} />
<PostList posts={posts} />
</div>
);
}
// ✅ الحل الصحيح: استخدام Suspense لفصل المكونات
async function DashboardPage() {
return (
<div>
<Suspense fallback={<UserProfileSkeleton />}>
<UserProfileLoader />
</Suspense>
<Suspense fallback={<PostListSkeleton />}>
<PostListLoader />
</Suspense>
</div>
);
}
async function UserProfileLoader() {
const user = await getCurrentUser();
return <UserProfile user={user} />;
}الـ Blocking Calls هي الكابوس الأكبر في تطبيقات Streaming. عندما تستخدم await داخل مكون، فإنك توقف تنفيذ بقية الكود حتى تكتمل العملية، وهذا يعني أن المتصفح لا يتلقى أي بيانات حتى تنتهي جميع الـ awaits. الحل هو استخدام Promise.all لتنفيذ العمليات المتوازية، أو استخدام مكتبات مثل react-streaming لإدارة الـ Streaming بشكل أكثر كفاءة.
في أحد المشاريع الكبيرة، كنا نستخدم GraphQL للحصول على البيانات من عدة مصادر مختلفة. المشكلة كانت أن كل استعلام كان يتم تنفيذه بشكل تسلسلي، مما يؤدي إلى وقت تحميل يصل إلى ٨ ثوانٍ. بعد إعادة هيكلة الكود لاستخدام Promise.all، انخفض وقت التحميل إلى أقل من ثانيتين. هذا يوضح أن Streaming لا يتعلق فقط بوضع المكونات داخل Suspense، بل يتعلق أيضاً بكيفية جلب البيانات في المقام الأول.
// استخدام Promise.all لتنفيذ العمليات المتوازية
async function DashboardPage() {
// جلب البيانات بشكل متوازٍ بدلاً من تسلسلي
const [user, posts, notifications] = await Promise.all([
getCurrentUser(),
getRecentPosts(),
getNotifications(),
]);
return (
<div>
<UserProfile user={user} />
<PostList posts={posts} />
<NotificationList notificati{notifications} />
</div>
);
}App Router يقدم العديد من الميزات المتقدمة التي يمكن أن تحسن أداء تطبيقك بشكل كبير، لكن معظم المطورين لا يعرفون عنها أو لا يستخدمونها بشكل صحيح. من بين هذه الميزات: الـ Dynamic Routes مع الـ generateStaticParams، والـ Incremental Static Regeneration (ISR)، والـ Edge Runtime. كل واحدة من هذه الميزات لها استخداماتها الخاصة، ويجب فهمها بعمق لتجنب الوقوع في فخاخ الأداء.
على سبيل المثال، الـ generateStaticParams هي طريقة لتوليد مسارات ثابتة في وقت البناء، وهي مفيدة جداً للمواقع التي تحتوي على عدد كبير من الصفحات الثابتة مثل المدونات أو مواقع التجارة الإلكترونية. لكن المشكلة هي أن الكثير من المطورين يستخدمونها دون فهم كيفية عملها خلف الكواليس. في أحد المشاريع، كنا نستخدم generateStaticParams لتوليد صفحات المنتجات، لكننا واجهنا مشكلة عندما زاد عدد المنتجات إلى أكثر من ١٠ آلاف منتج، مما أدى إلى زيادة وقت البناء إلى أكثر من ٢٠ دقيقة. الحل كان استخدام technique تسمى "batch generation"، حيث نقوم بتوليد الصفحات في دفعات بدلاً من توليدها جميعاً في وقت واحد.
// استخدام generateStaticParams لتوليد مسارات ثابتة
// ملف: app/products/[id]/page.tsx
export async function generateStaticParams() {
// جلب قائمة المنتجات من قاعدة البيانات
const products = await db.product.findMany({ select: { id: true } });
// توليد مسارات ثابتة لكل منتج
return products.map((product) => ({
id: product.id,
}));
}
async function ProductPage({ params }: { params: { id: string } }) {
const product = await db.product.findUnique({
where: { id: params.id },
});
if (!product) {
notFound();
}
return <ProductDetails product={product} />;
}Edge Runtime هو بيئة تنفيذ جديدة في Next.js تعمل على خوادم Edge مثل Cloudflare Workers أو Vercel Edge Network. الفرق الرئيسي بينها وبين Node.js Runtime هو أن Edge Runtime لا يدعم جميع APIs التي يدعمها Node.js، لكنها توفر زمن استجابة أقل بكثير لأنها تعمل أقرب إلى المستخدم. استخدم Edge Runtime عندما تحتاج إلى تنفيذ عمليات بسيطة وسريعة مثل إعادة التوجيه أو معالجة الطلبات البسيطة، ولكن تجنب استخدامها للعمليات التي تتطلب Node.js APIs مثل الوصول إلى نظام الملفات أو استخدام مكتبات تعتمد على Node.js.
في أحد المشاريع، كنا نستخدم Edge Runtime لإعادة التوجيه بناءً على لغة المستخدم. العملية كانت تستغرق أقل من ٥٠ مللي ثانية، مقارنة بـ ٣٠٠ مللي ثانية عند استخدام Node.js Runtime. لكن عندما حاولنا استخدام Edge Runtime لمعالجة الصور باستخدام sharp، واجهنا مشاكل لأن sharp يعتمد على Node.js APIs التي لا تتوفر في Edge Runtime. هذا يوضح أن Edge Runtime ليست بديلاً لـ Node.js Runtime، بل هي أداة إضافية يجب استخدامها بحكمة.
// استخدام Edge Runtime لإعادة التوجيه
// ملف: app/api/redirect/route.ts
export const runtime = 'edge';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const lang = searchParams.get('lang') || 'en';
// إعادة التوجيه بناءً على اللغة
return Response.redirect(`/${lang}/dashboard`);
}بعد أكثر من عام من العمل مع App Router في مشاريع حقيقية، هذه هي النصائح التي أتمنى لو عرفتها منذ البداية: أولاً، لا تستخدم await داخل مكونات Server Components إلا إذا كنت بحاجة فعلية إليها، لأن هذا يعطل Streaming ويجعل تطبيقك أبطأ. ثانياً، صمم شجرتك المكونية بحيث تكون الـ Client Components في أسفل الشجرة، وتتلقى البيانات فقط من الـ Server Components، وليس العكس. ثالثاً، استخدم Server Actions فقط للعمليات البسيطة والسريعة، واستخدم Route Handlers للعمليات الطويلة أو التي تتطلب تحكم أكبر في الـ Response.
وأخيراً، تذكر أن App Router ليس مجرد تحديث بسيط، بل هو إعادة تفكير كاملة في كيفية بناء تطبيقات React. إذا كنت لا تزال تستخدمه بنفس العقلية القديمة لـ Pages Router، فأنت تخسر الكثير من ميزاته القوية. ابدأ بمشروع صغير، جرب مختلف الميزات، وافهم كيف تعمل خلف الكواليس. ، ستتمكن من بناء تطبيقات سريعة وفعالة باستخدام Next.js App Router في ٢٠٢٥ وما بعده.