نافذة Modal بـ Intercepting Routes في Next.js: نماذج لوحة تحكم SaaS
تعلم بناء نوافذ Next.js intercepting routes منبثقة تدعم الروابط العميقة وتحفظ الحالة باستخدام parallel slots وServer Actions وshadcn/ui للوحات تحكم SaaS.

يعني بناء لوحات تحكم SaaS غنية بالبيانات أن المستخدمين يقومون بتعديل السجلات أو عرض الملفات الشخصية أو ضبط الإعدادات بشكل متكرر. عندما تقوم بتركيب Dialog تقليدي في React، فإنك تحجز المستخدم: إذا قام بتحديث الصفحة، أو شارك الرابط، أو ضغط على زر الرجوع في المتصفح، فإنه يفقد سياقه ويتعطل حال التطبيق (application state). ولحل هذه المشكلة، يحتاج المطورون إلى استخدام نمط Next.js intercepting routes modal القوي.
ما هو الـ Next.js intercepting routes modal؟
إن Next.js intercepting routes modal هو نمط توجيه (routing pattern) يسمح بتحميل محتوى من جزء آخر من التطبيق داخل التخطيط (layout) الحالي، مع الحفاظ على السياق وإخفاء عنوان URL الحقيقي.
عندما ينتقل المستخدم إلى /tasks/123، يقوم Next.js باعتراض الطلب ورسم واجهة المهمة داخل فتحة تخطيط موازية (parallel layout slot) فوق العرض الحالي. يوفر هذا تجربة مستخدم مشابهة للـ client-side dialog السريع مع المزايا الهيكلية لعنوان URL يتم تقديمه من الخادم (server-rendered URL).
يحل هذا النمط مشكلة تطبيقات الصفحة الواحدة (SPA) الكلاسيكية حيث تفتقر الـ modals إلى روابط قابلة للمشاركة وتتعطل عند الضغط على زر الرجوع في المتصفح. من خلال ربط الطبقة الفوقية بجزء فعلي من المسار، يحافظ المطورون على قابلية الربط العميق (deep linkability) دون التضحية بالانتقالات السلسة التي تشبه التطبيقات الأصلية.
لماذا تفشل الـ React Modals التقليدية في لوحات تحكم الـ SaaS
تاريخياً، قام مطورو الواجهات الأمامية بتنفيذ الـ dialogs عن طريق تبديل متغير حالة منطقي (boolean state). ورغم أنها عملية جداً للتنبيهات البسيطة أو التأكيدات، إلا أن هذا النمط ينهار في بيئات Next.js App Router المعقدة.
تخيل سيناريو يقوم فيه المستخدم بتصفية جدول بيانات يحتوي على 500 صف، وتطبيق معايير بحث معقدة، ثم النقر على "تعديل" في الصف رقم 42. يتوقع المستخدم أن يظل الجدول كما هو في الخلفية. إذا ضغط بطريق الخطأ على زر الرجوع في المتصفح لإغلاق الـ modal، فإن الـ dialog الذي يعتمد على الـ boolean state سيخرجه تماماً من لوحة التحكم. علاوة على ذلك، لا يمكن لموظفي الدعم مشاركة رابط مباشر لهذا السجل المحدد لأن عنوان URL لم يتغير أبداً.
الانتقال إلى معمارية تعتمد على URL يتكامل بشكل طبيعي مع سجل المتصفح (history stack). لنقارن بين النهجين مباشرة.
| الميزة | الـ Dialog التقليدي بـ useState |
الـ Intercepting Routes |
|---|---|---|
| زر الرجوع في المتصفح | يخرج من تطبيق لوحة التحكم بالكامل | يغلق طبقة الـ dialog بأمان |
| مشاركة الرابط المباشر | مستحيل (يتطلب تعيين الحالة يدوياً) | أصلي (ينتقل إلى صفحة مستقلة) |
| تحديث الصفحة | يعيد ضبط حالة التطبيق بالكامل | يعرض عرض السجل المحدد |
| سياق الخلفية | لا يتأثر بصرياً، لكنه هش في السجل | محفوظ بشكل أصلي عبر الـ parallel routes |
| تأثير الأداء | يجمع كود الـ modal في الكتلة الرئيسية | يتم تقسيم الكود حسب جزء المسار تلقائياً |
مقارنة بين الـ Parallel Routes والـ Intercepting Routes في Next.js: ما هو الفرق؟
بناء dialog يعتمد على URL يتطلب ميزتين متمايزتين في Next.js تعملان معاً: parallel routes و intercepting routes. ورغم أنه يتم مناقشتهما غالباً بشكل متبادل، إلا أنهما يخدمان أغراضاً مختلفة تماماً في الإطار العملي.
تستخدم الـ Next.js parallel routes فتحات مسمى (named slots)، تبدأ برمز @. تسمح لك هذه الفتحات برسم عدة صفحات في نفس التخطيط في وقت واحد. عندما يتنقل المستخدم، يمكن لـ Next.js تحديث الفتحة الموازية دون إعادة رسم أو فقدان حالة الفتحة الأساسية children.
ومع ذلك، فإن الـ parallel route بمفرده يحجز مساحة هيكلية فقط. نحتاج أيضاً إلى "اختطاف" نية التوجيه نفسها. وهنا يأتي دور الـ intercepting routes. باستخدام اتفاقيات المجلدات مثل (.)، (..)، (..)(..)، أو (...)، فإنك توجه Next.js لـ "اعتراض" حدث التنقل ورسم محتوى الوجهة في فتحة محددة.
تعريف اتفاقيات نظام الملفات (Filesystem Conventions)
يجب أن يعكس مجلد الاعتراض (intercepting folder) المستوى النسبي للجزء المستهدف في تسلسل المسارات الهرمي.
(.)يطابق الأجزاء في نفس المستوى.(..)يطابق الأجزاء فوق المستوى الحالي بمرة واحدة.(..)(..)يطابق الأجزاء فوق المستوى الحالي بمرتين.(...)يطابق الأجزاء من مجلد root app.
إليك هيكل مجلدات جاهز للإنتاج يوضح المسارات القياسية ونظيراتها المعترضة.
app/
├── layout.tsx
├── @modal/ // The parallel route slot
│ ├── default.tsx // Fallback for unmatched slots
│ └── (.)tasks/ // Intercepting same-level route
│ └── [id]/
│ └── page.tsx // The modal UI component
└── tasks/
├── page.tsx // The primary background view
└── [id]/
└── page.tsx // Standalone full-page view
في هذا الهيكل، يقوم (.)tasks/[id] باعتراض التنقل السلس إلى /tasks/[id]. ولأنه يقع داخل الفتحة @modal، يتم رسم المكون المعترض جنباً إلى جنب مع tasks/page.tsx.
بناء Edit Modal قابلة للمشاركة باستخدام shadcn/ui Dialog
يتطلب دمج مكتبة واجهة مستخدم مثل shadcn/ui جسراً معماريًا محددًا. تتوقع مكتبة Radix UI، التي تشغل أساسيات الـ dialog، التحكم في حالة الرؤية الخاصة بها. يجب علينا عكس هذا التحكم بحيث يملي موجه Next.js متى يتم تركيب الـ modal وفكه.
بما أن الـ intercepting routes تنطبق فقط على التنقل السلس (النقر على <Link> من Next.js)، يجب أن يفتح الـ dialog تلقائياً عند التركيب. عندما يتفاعل المستخدم مع الواجهة لإغلاق الـ modal، نستخدم الموجه (router) لإزالة الطبقة الفوقية.
إنشاء مكون الـ Modal Wrapper
يجب عليك تعيين defaultOpen={true} وتجاوز معالج onOpenChange. إذا حاول الـ dialog الإغلاق، فإننا نعترض هذا الحدث ونقوم بتفعيل التنقل في السجل. هذا يؤدي إلى العودة في سجل المتصفح بدلاً من مجرد إخفاء عناصر DOM بصرياً.
"use client";import { useRouter } from "next/navigation";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { ReactNode } from "react";
interface InterceptedModalProps {
children: ReactNode;
title: string;
}
export function InterceptedModal({ children, title }: InterceptedModalProps) {
const router = useRouter();
const handleOpenChange = (isOpen: boolean) => {
if (!isOpen) {
// Cleanly exits the intercepted route by traversing history stack
router.back();
}
};
return (
<Dialog defaultOpen={true} onOpenChange={handleOpenChange}>
<DialogTitle className="sr-only">{title}</DialogTitle>
<DialogContent className="sm:max-w-[600px]">
{children}
</DialogContent>
</Dialog>
);
}
التعامل مع التنقل الصعب (Hard Navigation): ملف الـ default.tsx الاحتياطي
يفرض Next.js App Router قواعد مطابقة صارمة حول فتحات المسارات الموازية. عندما يقوم المستخدم بتحديث المتصفح أو لصق عنوان URL مباشرة في شريط العنوان، يتجاهل الإطار العملي المعترض (interceptor) تماماً ويرسم عرض الصفحة المستقلة بالكامل.
في Next.js 16، تحتاج فتحات الـ parallel route إلى ملف default.tsx كبديل عندما قد تكون الفتحة غير مطابقة عند التنقل الصعب. تشير التوثيقات الرسمية إلى أن ملف default.tsx يُستخدم كبديل عندما لا يتمكن Next.js من استعادة الحالة النشطة للفتحة بعد تحميل مباشر. عدم توفير هذا الملف للفتحة المتأثرة يؤدي إلى خطأ في البناء (build error).
ملف default.tsx
يضمن ملف default.tsx الذي يعيد null داخل الفتحة @modal بقاء الفتحة مخفية عندما لا يتطابق أي مسار معترض.
// app/@modal/default.tsx
export default function ModalDefault() {
// Returns null to ensure the overlay remains invisible
// when no intercepting route is actively matching the URL.
return null;
}
إدارة الـ Form Mutations والـ Server Actions داخل الـ Modal
يتطلب إرسال Server Action داخل modal معترض حذراً شديداً. استخدام revalidatePath مباشرة يمكن أن يعيد ضبط فتحة التخطيط قبل الأوان.
في Next.js 16، النهج الحديث للاتساق الفوري هو API الـ updateTag. بينما يُستخدم revalidateTag(tag, "max") لتحديثات الخلفية (stale-while-revalidate)، تم تصميم updateTag(tag) لسيناريوهات "قراءة ما كتبته" (read-your-own-writes) مثل إرسال النماذج حيث يتوقع المستخدم تحديثاً فورياً.
الإغلاق بأمان بعد الإرسال
ادمج Server Action مع التنقل في السجل من جهة العميل باستخدام هوك useActionState من React 19.
"use client";import { useActionState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { updateTaskAction } from "@/actions/tasks";
import { Button } from "@/components/ui/button";
export function EditTaskForm({ taskId, initialData }: { taskId: string, initialData: string }) {
const router = useRouter();
const [state, formAction, isPending] = useActionState(updateTaskAction, null);
useEffect(() => {
if (state?.success) {
// Mutation succeeded; close modal via history pop
router.back();
}
}, [state?.success, router]);
return (
<form action={formAction} className="space-y-4">
<input type="hidden" name="id" value={taskId} />
<div className="flex flex-col gap-2">
<label htmlFor="title" className="text-sm font-medium">Task Title</label>
<input
id="title"
name="title"
defaultValue={initialData}
className="border p-2 rounded-md"
/>
</div>
<Button type="submit" disabled={isPending}>
{isPending ? "Saving..." : "Save Changes"}
</Button>
</form>
);
}
داخل الـ Server Action الخاص بك، استخدم updateTag('tasks') لإنهاء صلاحية ذاكرة التخزين المؤقت فوراً. يضمن ذلك أن ينتظر الطلب التالي البيانات الجديدة.
// actions/tasks.ts
"use server";import { updateTag } from "next/cache";
export async function updateTaskAction(prevState: any, formData: FormData) {
// ... database logic ...
// updateTag is designed for immediate consistency in actions
updateTag("tasks");
return { success: true };
}
أخطاء شائعة
- استخدام Route Groups بدلاً من Interceptors: أسماء المجلدات مثل
(modal)هي Route Groups. يجب استخدام(.)،(..)، أو(...)لتوجيه الموجه للاعتراض. - نسيان بديل
default.tsx: يتطلب Next.js 16 هذا للملفات الموازية غير المطابقة عند التحميل المباشر؛ حذفه يؤدي إلى أخطاء بناء. - استخدام
revalidateTagببارامتر واحد: هذا التوقيع مهجور في Next.js 16. استخدمrevalidateTag(tag, "max")لـ SWR أوupdateTag(tag)للإبطال الفوري في الـ actions. - فقدان
defaultOpen={true}: بدون هذا، يتم تركيب الـ dialogs المبنية على Radix ولكنها تبقى غير مرئية، مما يعطل واجهة المستخدم في الخلفية.
الأسئلة الشائعة
ما هو الـ intercepting route في Next.js؟
يسمح لك المسار المعترض بتحميل محتوى من جزء آخر من تطبيقك داخل التخطيط الحالي مع الحفاظ على السياق. تحدد الاتفاقيات مثل (.) أو (..) عدد الأجزاء التي يجب صعودها في الهيكل للمطابقة.
كيف أغلق modal في Next.js intercepting routes؟
استخدم router.back() من next/navigation. بما أن الـ modal مرتبط بجزء من المسار، فإن الرجوع في سجل التاريخ يؤدي إلى فك تركيب الفتحة واستعادة عنوان URL السابق.
ما هو الغرض من default.tsx؟
يعمل ملف default.tsx كبديل لفتحات الـ parallel route أثناء التنقل الصعب (مثل تحديث الصفحة) عندما لا يتمكن Next.js من تحديد الحالة النشطة لتلك الفتحة.
متى يجب أن أستخدم updateTag مقابل revalidateTag؟
استخدم updateTag(tag) داخل الـ Server Actions للاتساق الفوري (قراءة ما كتبته). استخدم revalidateTag(tag, "max") لتحديثات الخلفية حيث يكون تقديم محتوى قديم مؤقتاً مقبولاً من أجل الأداء.
الخاتمة
إن إتقان الـ modals المعتمدة على URL يغير طريقة تفاعل المستخدمين مع لوحات التحكم الخاصة بك. من خلال دمج فتحات @modal، واتفاقيات الاعتراض، والتنقل البرمجي في السجل، فإنك توفر إمكانية ربط عميق دون التضحية بالشعور السلس لتطبيقات الصفحة الواحدة.
إذا كنت ترغب في اختصار الوقت وشحن واجهات العملاء بشكل أسرع، استكشف قوالب لوحات التحكم الجاهزة للإنتاج لدينا في Aniq UI. فهي تأتي معدة مسبقاً مع معماريات توجيه Next.js 16، ونماذج React 19، وأنظمة تصميم قوية من البداية.


