هل تعتقد أن ترحيل مشروعك إلى App Router مجرد تحديث روتيني؟ الحقيقة الصادمة هي أن 68% من المشاريع التي انتقلت دون فهم عميق تعاني من تسرب ذاكرة مزمن وتحميل صفحات أبطأ بـ 40%. هذا الدليل سيجعلك تتقن الـ App Router من الداخل: من الـ Server Components إلى الـ Streaming، مع أمثلة حقيقية وحلول للأخطاء التي
قبل أن نتعمق في التفاصيل، دعنا نفهم لماذا قرر فريق Next.js تغيير النظام بالكامل. المشكلة الأساسية مع Pages Router هي أنه يعتمد على نموذج تحميل الصفحات بالكامل (Page-based Routing) حيث يتم تحميل كل صفحة كملف مستقل، مع كل الجافا سكريبت المطلوب لتشغيلها. هذا النموذج كان يعمل بشكل جيد في الماضي، لكنه أصبح غير صالح للتطبيقات الحديثة المعقدة التي تتطلب تحميل بيانات ديناميكية وتفاعلية عالية. على سبيل المثال، في Pages Router، إذا كان لديك صفحة تحتوي على مكونات متعددة تستدعي بيانات من واجهة برمجة تطبيقات API، فإن كل هذه المكونات ستعمل على العميل، مما يؤدي إلى تحميل زائد للجافا سكريبت ووقت تنفيذ أطول.
الـ App Router جاء ليحل هذه المشكلة من خلال اعتماد نموذج قائم على المكونات (Component-based Routing) حيث يمكن تقسيم الصفحة إلى مكونات صغيرة، بعضها يعمل على السيرفر وبعضها على العميل. هذا يعني أن جزءاً كبيراً من العمل يتم على السيرفر، مما يقلل من كمية الجافا سكريبت المرسلة إلى المتصفح ويحسن أداء التطبيق بشكل كبير. لكن هذا التحول لم يكن مجرد تغيير في البنية، بل تطلب إعادة التفكير في كيفية بناء التطبيقات بالكامل، بدءاً من كيفية جلب البيانات إلى كيفية التعامل مع التفاعلية.
لنأخذ مثالاً عملياً: تطبيق بسيط لعرض قائمة المنتجات مع إمكانية البحث والتصفية. في Pages Router، ستحتاج إلى تحميل مكتبة React بالكامل مع كل المكونات التي تعتمد عليها، بالإضافة إلى أي مكتبات أخرى مثل axios أو Zustand لإدارة الحالة. في هذا السيناريو، حجم الجافا سكريبت قد يصل إلى 2.5 ميجابايت أو أكثر، مع وقت تحميل يتجاوز 2 ثانية على شبكات 4G. أما في App Router، يمكنك جعل معظم المكونات تعمل على السيرفر، بحيث يتم إرسال HTML جاهز للمتصفح، مع كمية جافا سكريبت أقل بكثير (ربما 300 كيلوبايت فقط). النتيجة؟ وقت تحميل يقل عن 800 مللي ثانية، مع تحسين كبير في تجربة المستخدم.
// مثال على صفحة في Pages Router (قديمة)
import { useState, useEffect } from 'react';
import axios from 'axios';
export default function ProductsPage() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
axios.get('/api/products').then((res) => {
setProducts(res.data);
setLoading(false);
});
}, []);
if (loading) return <div>جاري التحميل...</div>;
return (
<div>
{products.map((product) => (
<div key={product.id}>{product.name}</div>
))}
</div>
);
}// نفس الصفحة في App Router (جديدة)
// ملف: app/products/page.js
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 } // تحديث البيانات كل ساعة
});
if (!res.ok) throw new Error('فشل في جلب البيانات');
return res.json();
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<div>
{products.map((product) => (
<div key={product.id}>{product.name}</div>
))}
</div>
);
}لاحظ الفرق بين المثالين: في Pages Router، يتم جلب البيانات على العميل باستخدام useEffect و axios، مما يعني أن المتصفح يجب أن ينتظر تحميل الجافا سكريبت ثم يقوم بطلب البيانات. أما في App Router، يتم جلب البيانات على السيرفر باستخدام دالة async عادية، ويتم إرسال HTML جاهز للمتصفح. هذا الفرق ليس مجرد تحسين بسيط، بل هو تغيير جوهري في كيفية عمل التطبيق. لكن هذا ليس كل شيء، فالـ App Router يقدم ميزات أخرى مثل الـ Streaming والـ Server Actions التي سنتناولها لاحقاً.
الـ Server Components هي الميزة الأهم في App Router، وهي التي تسمح بتشغيل جزء من مكونات React على السيرفر بدلاً من العميل. لكن كيف تعمل هذه المكونات بالضبط؟ دعنا نفهم ما يحدث خلف الكواليس. عندما يطلب المتصفح صفحة من تطبيق Next.js باستخدام App Router، يقوم السيرفر بتشغيل جميع الـ Server Components أولاً، ثم يقوم بتحويلها إلى HTML عادي يرسل إلى المتصفح. هذا يعني أن المتصفح لا يحتاج إلى تحميل أو تنفيذ أي جافا سكريبت لهذه المكونات، مما يقلل من حجم الجافا سكريبت المرسل ويحسن الأداء بشكل كبير.
لكن هناك نقطة مهمة يجب فهمها: الـ Server Components ليست مجرد مكونات عادية تعمل على السيرفر. في الواقع، هذه المكونات لا تدعم أي تفاعلية على العميل، مثل استخدام useState أو useEffect أو أي من hooks الخاصة بالعميل. إذا حاولت استخدام هذه الـ hooks في Server Component، ستحصل على خطأ في وقت البناء (Build Time). هذا يعني أنك بحاجة إلى تقسيم مكوناتك بعناية بين السيرفر والعميل، حيث تستخدم Server Components للأجزاء الثابتة أو التي تعتمد على بيانات من السيرفر، وتستخدم Client Components للأجزاء التفاعلية.
إحدى الفخاخ الشائعة التي يقع فيها المطورون الجدد هي محاولة تمرير Server Components كchildren إلى Client Components. هذا لن يعمل لأن Client Components لا يمكنها فهم Server Components. بدلاً من ذلك، يجب عليك تحويل Server Component إلى Client Component إذا كنت بحاجة إلى تمريرها كchildren، أو استخدام نمط مختلف مثل تمرير البيانات كـ props. دعنا نرى مثالاً عملياً:
// ملف: app/products/ProductList.js (Server Component)
async function getProducts() {
const res = await fetch('https://api.example.com/products');
return res.json();
}
export default async function ProductList() {
const products = await getProducts();
return (
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}// ملف: app/products/page.js (Server Component)
import ProductList from './ProductList';
import FilterControls from './FilterControls.client';
export default function ProductsPage() {
return (
<div>
<h1>منتجاتنا</h1>
<FilterControls />
<ProductList />
</div>
);
}// ملف: app/products/FilterControls.client.js (Client Component)
'use client';
import { useState } from 'react';
export default function FilterControls() {
const [category, setCategory] = useState('all');
return (
<div>
<select value={category} {(e) => setCategory(e.target.value)}>
<option value="all">الكل</option>
<option value="electronics">إلكترونيات</option>
<option value="clothing">ملابس</option>
</select>
</div>
);
}في هذا المثال، ProductList هو Server Component يعمل على السيرفر، بينما FilterControls هو Client Component يعمل على العميل. لاحظ أننا استخدمنا امتداد .client.js للإشارة إلى أن هذا المكون يجب أن يعمل على العميل. هذا النمط يسمح لنا بالاستفادة من مزايا كلا النوعين من المكونات: Server Components لتحميل البيانات بسرعة وإرسال HTML جاهز، وClient Components للتفاعلية على العميل. لكن هناك مشكلة شائعة هنا: كيف يمكننا تمرير بيانات من Server Component إلى Client Component؟ هذا هو موضوع القسم التالي.
إذا كنت قد استخدمت تطبيقات مثل Twitter أو LinkedIn مؤخراً، فربما لاحظت أنها تعرض الهيكل الأساسي للصفحة بسرعة كبيرة، ثم تقوم بتحميل المحتوى تدريجياً. هذا هو بالضبط ما يفعله الـ Streaming في Next.js App Router. بدلاً من انتظار تحميل جميع البيانات قبل عرض الصفحة، يقوم الـ Streaming بإرسال أجزاء من الصفحة إلى المتصفح فور توفرها، مما يجعل التطبيق يشعر بأنه أسرع بكثير، حتى لو كان الوقت الإجمالي للتحميل هو نفسه.
لفهم كيف يعمل الـ Streaming، دعنا نتعمق قليلاً في ما يحدث خلف الكواليس. عندما يطلب المتصفح صفحة من تطبيق Next.js، يبدأ السيرفر في تشغيل الـ Server Components وتحميل البيانات المطلوبة. بدلاً من انتظار اكتمال جميع هذه العمليات، يقوم السيرفر بإرسال أجزاء من الصفحة إلى المتصفح فور توفرها. هذه الأجزاء تسمى "Chunks" ويتم إرسالها باستخدام بروتوكول HTTP/2 أو HTTP/3 الذي يدعم الـ Multiplexing. هذا يعني أن المتصفح يمكنه البدء في عرض أجزاء من الصفحة بينما لا تزال الأجزاء الأخرى قيد التحميل، مما يحسن تجربة المستخدم بشكل كبير.
لتنفيذ الـ Streaming في Next.js، نستخدم مكون React المدمج المسمى Suspense. هذا المكون يسمح لنا بتحديد أجزاء من الصفحة التي قد تستغرق وقتاً في التحميل، وتقديم محتوى بديل (مثل مؤشر تحميل) أثناء انتظار البيانات. دعنا نرى مثالاً عملياً:
// ملف: app/dashboard/page.js
import { Suspense } from 'react';
import DashboardStats from './DashboardStats';
import RecentActivity from './RecentActivity';
export default function DashboardPage() {
return (
<div>
<h1>لوحة التحكم</h1>
<Suspense fallback={<div>جاري تحميل الإحصائيات...</div>}>
<DashboardStats />
</Suspense>
<Suspense fallback={<div>جاري تحميل النشاط الأخير...</div>}>
<RecentActivity />
</Suspense>
</div>
);
}// ملف: app/dashboard/DashboardStats.js
async function getStats() {
// محاكاة تأخير في جلب البيانات
await new Promise((resolve) => setTimeout(resolve, 2000));
return { users: 1200, revenue: 45000 };
}
export default async function DashboardStats() {
const stats = await getStats();
return (
<div>
<p>عدد المستخدمين: {stats.users}</p>
<p>الإيرادات: ${stats.revenue}</p>
</div>
);
}في هذا المثال، DashboardStats سيستغرق ثانيتين لجلب البيانات، لكن بفضل Suspense، سيتم عرض بقية الصفحة (مثل العنوان والنشاط الأخير إذا كان جاهزاً) فوراً، مع عرض رسالة "جاري تحميل الإحصائيات..." أثناء انتظار البيانات. هذا يجعل التطبيق يشعر بأنه أسرع بكثير، حتى لو كان الوقت الإجمالي للتحميل هو نفسه. لكن الـ Streaming ليس الميزة الوحيدة التي تجعل التطبيقات تبدو أسرع، فهناك أيضاً الـ Server Actions التي سنتناولها الآن.
الـ Server Actions هي ميزة جديدة في Next.js تسمح لك بتشغيل وظائف على السيرفر مباشرة من مكونات العميل، دون الحاجة إلى إنشاء واجهة برمجة تطبيقات API منفصلة. هذا يبسط بشكل كبير عملية التعامل مع النماذج والبيانات، ويقلل من كمية الجافا سكريبت المطلوبة على العميل. لفهم كيف تعمل الـ Server Actions، دعنا نرى مثالاً عملياً:
// ملف: app/actions.js
'use server';
export async function createPost(formData) {
const title = formData.get('title');
const c formData.get('content');
// هنا يمكنك حفظ البيانات في قاعدة البيانات
console.log('تم إنشاء منشور:', { title, content });
return { success: true };
}// ملف: app/posts/CreatePostForm.client.js
'use client';
import { createPost } from '@/app/actions';
export default function CreatePostForm() {
return (
<form action={createPost}>
<input type="text" name="title" placeholder="العنوان" required />
<textarea name="content" placeholder="المحتوى" required></textarea>
<button type="submit">إنشاء المنشور</button>
</form>
);
}في هذا المثال، قمنا بتعريف Server Action باسم createPost باستخدام التوجيه 'use server'. هذه الدالة تعمل على السيرفر ويمكن استدعاؤها مباشرة من مكون العميل باستخدام خاصية action في عنصر form. عندما يرسل المستخدم النموذج، يتم إرسال البيانات إلى السيرفر حيث يتم معالجتها بواسطة دالة createPost. هذا النمط يبسط بشكل كبير عملية التعامل مع النماذج، ويقلل من الحاجة إلى كتابة كود جافا سكريبت إضافي على العميل. لكن هناك نقطة مهمة يجب الانتباه إليها: الـ Server Actions تعمل فقط مع النماذج التقليدية (Form Submission)، وليس مع الأحداث التفاعلية مثل onClick. إذا كنت بحاجة إلى تفاعل أكثر تعقيداً، فستحتاج إلى استخدام مزيج من Server Actions و Client Components.
إحدى الميزات القوية في Next.js App Router هي نظام الـ Caching المتقدم الذي يسمح لك بتخزين البيانات مؤقتاً وتحسين أداء التطبيق بشكل كبير. لكن هذا النظام قد يكون معقداً بعض الشيء في البداية، خاصةً عندما يتعلق الأمر بالـ Revalidation الذي يسمح لك بتحديث البيانات المخزنة مؤقتاً دون الحاجة إلى إعادة تحميل الصفحة بالكامل. لفهم كيف يعمل هذا النظام، دعنا نلقي نظرة على المثال التالي:
// ملف: app/products/page.js
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 60 } // إعادة التحقق كل 60 ثانية
});
return res.json();
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<div>
{products.map((product) => (
<div key={product.id}>{product.name}</div>
))}
</div>
);
}في هذا المثال، استخدمنا خيار next: { revalidate: 60 } مع دالة fetch. هذا يعني أن البيانات سيتم تخزينها مؤقتاً لمدة 60 ثانية، وبعد ذلك سيتم إعادة جلب البيانات من المصدر الأصلي في المرة التالية التي يتم فيها طلب الصفحة. هذا النمط مفيد جداً للبيانات التي تتغير بشكل متكرر، مثل قوائم المنتجات أو الأخبار. لكن هناك سيناريوهات أخرى قد تحتاج فيها إلى إعادة التحقق من البيانات بشكل فوري، مثل بعد إجراء عملية تعديل أو حذف. لهذا الغرض، يوفر Next.js دالة revalidatePath التي تسمح لك بإعادة التحقق من بيانات مسار معين بشكل فوري.
لنفترض أن لديك صفحة تعرض قائمة المنتجات، وقد أضفت منتجاً جديداً باستخدام Server Action. في هذه الحالة، ستحتاج إلى إعادة التحقق من بيانات الصفحة التي تعرض المنتجات لضمان عرض المنتج الجديد فوراً. إليك كيف يمكنك القيام بذلك:
// ملف: app/actions.js
'use server';
import { revalidatePath } from 'next/cache';
export async function createProduct(formData) {
const name = formData.get('name');
const price = formData.get('price');
// حفظ المنتج في قاعدة البيانات
await db.product.create({ data: { name, price } });
// إعادة التحقق من مسار المنتجات
revalidatePath('/products');
return { success: true };
}في هذا المثال، بعد حفظ المنتج الجديد في قاعدة البيانات، قمنا باستدعاء revalidatePath('/products') لإعادة التحقق من بيانات مسار /products. هذا يعني أنه في المرة التالية التي يتم فيها طلب هذه الصفحة، سيتم جلب البيانات الجديدة من المصدر بدلاً من استخدام البيانات المخزنة مؤقتاً. هذا النمط مفيد جداً للتطبيقات التي تتطلب تحديثات فورية، مثل لوحات التحكم أو التطبيقات التي تعتمد على بيانات ديناميكية.
بالإضافة إلى الـ Caching على مستوى الصفحة، يوفر Next.js أيضاً إمكانية تخزين البيانات مؤقتاً على مستوى المكونات باستخدام دالة cache. هذا مفيد بشكل خاص عندما يكون لديك مكونات متعددة تعتمد على نفس البيانات، وتريد تجنب جلب هذه البيانات أكثر من مرة. دعنا نرى مثالاً:
// ملف: app/lib/data.js
import { cache } from 'react';
export const getUser = cache(async (userId) => {
const res = await fetch(`https://api.example.com/users/${userId}`);
return res.json();
});// ملف: app/profile/UserProfile.js
import { getUser } from '@/app/lib/data';
export default async function UserProfile({ userId }) {
const user = await getUser(userId);
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}// ملف: app/profile/page.js
import UserProfile from './UserProfile';
export default async function ProfilePage({ searchParams }) {
const userId = searchParams.id;
return (
<div>
<UserProfile userId={userId} />
</div>
);
}في هذا المثال، استخدمنا دالة cache لتخزين نتيجة دالة getUser مؤقتاً. هذا يعني أنه إذا تم استدعاء هذه الدالة أكثر من مرة بنفس المعاملات (userId)، فسيتم إعادة استخدام النتيجة المخزنة مؤقتاً بدلاً من جلب البيانات مرة أخرى. هذا النمط مفيد جداً لتحسين أداء التطبيق وتقليل عدد الطلبات إلى واجهة برمجة التطبيقات API أو قاعدة البيانات.
على الرغم من أن Next.js App Router يقدم العديد من المزايا، إلا أنه يأتي أيضاً مع مجموعة من الفخاخ التي يمكن أن تسبب مشاكل كبيرة إذا لم تكن على دراية بها. في هذا القسم، سنتناول بعضاً من أكثر الأخطاء شيوعاً التي يقع فيها المطورون عند استخدام App Router، مع الحلول العملية لتجنبها.
أحد أكثر الأخطاء شيوعاً هو محاولة استخدام hooks مثل useState أو useEffect في Server Components. هذا لن يعمل لأن هذه الـ hooks مصممة للعمل على العميل فقط، وستحصل على خطأ في وقت البناء إذا حاولت استخدامها في Server Component. الحل بسيط: قم بتحويل المكون إلى Client Component باستخدام التوجيه 'use client'، أو قم بفصل الجزء التفاعلي إلى مكون منفصل يعمل على العميل.
// ❌ خطأ: استخدام useState في Server Component
// ملف: app/counter/page.js
import { useState } from 'react';
export default function CounterPage() {
const [count, setCount] = useState(0); // سيؤدي إلى خطأ في وقت البناء
return (
<div>
<p>العداد: {count}</p>
<button {() => setCount(count + 1)}>زيادة</button>
</div>
);
}// ✅ صحيح: تحويل المكون إلى Client Component
// ملف: app/counter/Counter.client.js
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>العداد: {count}</p>
<button {() => setCount(count + 1)}>زيادة</button>
</div>
);
}إذا كان لديك مكون يستغرق وقتاً طويلاً لجلب البيانات، مثل تقرير كبير أو قائمة طويلة من العناصر، فإن تجاهل استخدام الـ Streaming يمكن أن يؤدي إلى تجربة مستخدم سيئة حيث ينتظر المستخدم تحميل الصفحة بالكامل قبل رؤية أي شيء. الحل هو استخدام مكون Suspense لتقسيم الصفحة إلى أجزاء يمكن تحميلها بشكل مستقل، كما رأينا في الأقسام السابقة.
عندما تقوم بتعديل البيانات باستخدام Server Action، قد لا ترى التغييرات فوراً في الصفحات الأخرى إذا كنت تعتمد على الـ Caching. هذا لأن البيانات المخزنة مؤقتاً قد لا تعكس التغييرات الجديدة. الحل هو استخدام revalidatePath لإعادة التحقق من البيانات بعد إجراء أي تعديل، كما رأينا في مثال createProduct السابق.
بعض المطورين يميلون إلى تحويل جميع مكوناتهم إلى Client Components حتى لو لم تكن بحاجة إلى تفاعلية. هذا خطأ لأن Client Components تتطلب تحميل وتنفيذ جافا سكريبت على العميل، مما يزيد من حجم الجافا سكريبت ويؤثر على الأداء. الحل هو استخدام Server Components قدر الإمكان للأجزاء الثابتة أو التي تعتمد على بيانات من السيرفر، واستخدام Client Components فقط للأجزاء التي تتطلب تفاعلية حقيقية.
في Pages Router، كان من السهل التعامل مع الأخطاء باستخدام try/catch في useEffect. لكن في App Router، إذا حدث خطأ في Server Component، فقد يؤدي ذلك إلى فشل الصفحة بالكامل. الحل هو استخدام مكون Error Boundary المدمج في Next.js للتعامل مع الأخطاء بشكل أنيق:
// ملف: app/products/error.js
'use client';
export default function Error({ error, reset }) {
return (
<div>
<h2>حدث خطأ!</h2>
<p>{error.message}</p>
<button {() => reset()}>إعادة المحاولة</button>
</div>
);
}هذا المكون سيتم عرضه إذا حدث خطأ في أي من مكونات الصفحة، مما يسمح للمستخدم بإعادة المحاولة أو رؤية رسالة خطأ مفيدة بدلاً من صفحة بيضاء.
بعد استكشاف جميع هذه الميزات والفخاخ، إليك بعض النصائح العملية التي ستساعدك على تطبيق Next.js App Router بنجاح في مشاريعك:
في النهاية، Next.js App Router ليس مجرد تحديث بسيط، بل هو تحول جذري في كيفية بناء التطبيقات الحديثة. إذا تم استخدامه بشكل صحيح، يمكن أن يحسن أداء تطبيقك بشكل كبير ويجعل تجربة المستخدم أكثر سلاسة. لكن هذا يتطلب فهماً عميقاً لكيفية عمله خلف الكواليس، وليس مجرد نسخ ولصق الأمثلة من الوثائق. خذ وقتك لفهم هذه المفاهيم، وجربها في مشاريع صغيرة قبل تطبيقها على مشاريع كبيرة، وستجد أن الانتقال إلى App Router كان أحد أفضل القرارات التي اتخذتها لمشروعك