Parallel Routes ו-Intercepting Routes ב-Next.js 16: המדריך המלא ל-App Router

המדריך המלא ל-Parallel Routes ו-Intercepting Routes ב-Next.js 16 App Router: מודאלים עם deep linking, dashboards עם slots דינמיים ו-TypeScript.

Parallel Routes ב-Next.js 16 (מדריך 2026)

עודכן: 15 בספטמבר, 2026

Parallel Routes ו-Intercepting Routes הם שתי תכונות מתקדמות של App Router ב-Next.js 16 שמאפשרות רינדור סימולטני של מספר עמודים באותו layout, ויירוט של ניווט להצגת תוכן בקונטקסט אחר (למשל מודאל מעל העמוד הנוכחי). Parallel Routes משתמשים בתחביר @folder ליצירת "סלוטים" (slots) שמועברים ל-layout כ-props, בעוד ש-Intercepting Routes משתמשים במוסכמות (.), (..) ו-(...) לתפיסת URL לפי רמת התיקייה. בפוסט הזה אני עובר על שני המנגנונים לעומק, עם דוגמאות עובדות של מודאלים, dashboards עם מספר sections, ו-deep-linking שמחזיק גם אחרי refresh.

  • Parallel Routes מוגדרים באמצעות תיקיות שמתחילות ב-@ (למשל @analytics) ומועברים כ-props ל-layout שמעליהם.
  • כל parallel route מחייב קובץ default.tsx כדי לתמוך ברענון עמוד (hard navigation). בלעדיו, תקבלו 404.
  • Intercepting Routes משתמשים בתחביר (.) (אותה רמה), (..) (רמה מעל), ו-(...) (מהשורש) כדי ליירט URL של soft navigation.
  • הקומבינציה של parallel plus intercepting היא הדרך המומלצת ב-Next.js 16 לבנות מודאלים עם deep linking שעובד גם ב-refresh וגם בשיתוף קישור.
  • Parallel Routes תומכים במלואם ב-React Server Components, streaming עם loading.tsx, וקבצי error.tsx נפרדים לכל slot.
  • Slots הם בעצם children קונטקסטואליים. ה-router ממפה כל slot לפי ה-URL הנוכחי או לפי ה-default.tsx שלו.

מה זה Parallel Routes ב-Next.js 16?

Parallel Routes הם מנגנון של App Router שמאפשר לרנדר בו-זמנית מספר עמודים עצמאיים בתוך אותו layout, כאשר לכל עמוד יכולים להיות מצב טעינה, שגיאה וניווט משלו. במקום שכל route יחליף את התוכן של {children}, אתם יכולים להגדיר "slots", כלומר פרופס נוספים שה-router מזין ל-layout שלכם. Slot מוגדר על ידי יצירת תיקייה שמתחילה ב-@, למשל @analytics או @notifications.

המקרים הכי נפוצים לשימוש הם dashboards מורכבים שבהם sidebar, feed ראשי ו-panel של הודעות צריכים לנווט באופן עצמאי, וכן split views כמו מסך של קריאה פלוס עריכה. ב-תיעוד הרשמי של Parallel Routes ב-Next.js תמצאו את הסכמה הגרפית המלאה, אבל הדרך הכי טובה להבין את זה היא לבנות דוגמה אמיתית. אני אעשה בדיוק את זה בהמשך המדריך.

מבחינה קונספטואלית, כל slot הוא route tree עצמאי משלו. יש לו page.tsx, יכולים להיות לו loading.tsx ו-error.tsx משלו, והוא יכול אפילו להכיל layout.tsx משנה. Next.js אחראית לתאם ביניהם ולהעביר לכל slot את ה-JSX המתאים לפי ה-URL הנוכחי. אם ה-URL לא תואם לאף route בתוך slot, ה-router נופל ל-default.tsx של אותו slot, קובץ שהוא חובה בכל parallel route (עוד על זה בהמשך).

מה ההבדל בין Parallel Routes ל-Intercepting Routes?

ההבדל המהותי הוא שה-Parallel Routes עוסקים במרחב. הם מגדירים אילו איזורים ברינדור מקבילים ומקבלים כל אחד את ה-URL שלו. Intercepting Routes, לעומת זאת, עוסקים בזמן וניווט. הם תופסים ניווט שהיה אמור להוליך לעמוד אחד ומציגים במקומו קומפוננט אחר (בדרך כלל מודאל), אבל רק כשהניווט מגיע דרך <Link> (soft navigation). ב-hard navigation (רענון או פתיחה של קישור בטאב חדש), ה-URL המקורי נטען כרגיל.

מאפייןParallel RoutesIntercepting Routes
תחביר תיקייה@slotName(.), (..), (...)
מטרה עיקריתרינדור סימולטני של אזורים מרוביםתפיסת ניווט להצגת UI מקביל
העברה ל-layoutכ-prop בשם ה-slotלא, פועל ברמת ה-route
מחייב default.tsxכןלא (אך לרוב משולב עם parallel)
תלוי בסוג ניווטלא, פועל תמידכן, רק ב-soft navigation
שימוש טיפוסיDashboards, split viewsמודאלים, previews, drawers

בפועל, שני המנגנונים משולבים ב-99% מהמקרים. פותחים @modal כ-parallel route, ובתוכו מגדירים intercepting route שתופס את ה-URL של עמוד הפריט. הרעיון הזה נשמע מסובך בכתיבה, אבל התוצאה היא UX חלק במיוחד: לחיצה על תמונה במסך רשת מציגה מודאל, שיתוף הקישור מהמודאל פותח את העמוד המלא, וכפתור back מחזיר בדיוק לאן שהמשתמש היה. הודה, שברתי את זה בעצמי בפרויקט אחד כי שכחתי את ה-default.tsx. לא כיף לגלות את זה בפרודקשן.

בניית slot מקבילי צעד אחר צעד

אז בואו נבנה dashboard פשוט עם שני slots: @feed להצגת מדדים בזמן אמת ו-@notifications לפאנל התראות. השלב הראשון הוא ליצור את מבנה התיקיות תחת app/dashboard:

app/
└─ dashboard/
   ├─ layout.tsx
   ├─ page.tsx
   ├─ @feed/
   │  ├─ default.tsx
   │  ├─ page.tsx
   │  └─ [metric]/page.tsx
   └─ @notifications/
      ├─ default.tsx
      └─ page.tsx

ה-layout מקבל את כל ה-slots כפרופס נפרדים לצד children:

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  feed,
  notifications,
}: {
  children: React.ReactNode
  feed: React.ReactNode
  notifications: React.ReactNode
}) {
  return (
    <div className="grid grid-cols-[1fr_320px] gap-6">
      <main>
        {children}
        <section className="mt-6">{feed}</section>
      </main>
      <aside>{notifications}</aside>
    </div>
  )
}

שימו לב שאין שום קונפיגורציה נוספת. ה-router של Next.js מזהה את התיקיות @feed ו-@notifications אוטומטית ומעביר אותן כ-props. ב-URL הנפרד שלכם, כמו /dashboard/latency, ה-slot של @feed ירנדר את [metric]/page.tsx, ואילו @notifications ירדר את ה-default.tsx שלו. אפשר גם לבנות ניווט לכל slot בנפרד באמצעות useSelectedLayoutSegments("feed"), hook שמחזיר את מקטע ה-URL הפעיל של slot ספציפי, שימושי לרינדור breadcrumbs או tabs מותאמים.

למה default.tsx הוא חובה?

ה-default.tsx הוא הרינדור שיוצג ל-slot כאשר ה-URL הנוכחי לא תואם לאף אחת מה-pages בתוכו. הסיבה שהוא חובה נעוצה בהבדל בין soft ל-hard navigation. ב-soft navigation, ה-router שומר את המצב של כל slot שאינו מושפע, אבל ב-hard navigation (רענון עמוד או פתיחה ישירה של URL), אין מצב שמור. ה-router חייב לדעת מה לרנדר לכל slot מאפס. בלי default.tsx, תקבלו 404 בכל refresh, גם כשהעמוד נפתח בעבר בהצלחה.

יש שתי אסטרטגיות מקובלות ל-default.tsx. או להחזיר null כשה-slot לא רלוונטי לעמוד הנוכחי (במקרה של מודאלים, למשל), או לרנדר את אותו התוכן כמו page.tsx ברמת השורש של ה-slot כדי שה-slot תמיד יראה משהו. הבחירה תלויה ב-UX שאתם רוצים. ב-Next.js 16, ה-router גם משתמש ב-default.tsx לצורך streaming של המצב ההתחלתי בזמן server rendering, מה שהופך את הבחירה שלכם ל-visible לפני שכל ה-slots הסתיימו להיטען.

תחביר Intercepting Routes: הבנת (.), (..) ו-(...)

Intercepting Routes משתמשים בסוגריים כדי לציין ממה מתחילה הספירה של הרמות, לא כדי ליצור route groups. זו נקודת בלבול מרכזית, כי route groups משתמשים באותם סוגריים בלי הנקודות. הנה השורה התחתונה:

  • (.): יורט route באותה רמה של התיקייה המקבילה. אם אתם בתוך @modal תחת /photos, אז (.)photo/[id] יתפוס את /photos/photo/123.
  • (..): יורט route ברמה אחת מעל. אם אתם ב-/photos/@modal ורוצים לתפוס /photos/[id], השתמשו ב-(..)[id].
  • (..)(..): שתי רמות מעל. שימו לב שזה לא סוגר אחד עם שתי נקודות; זה שני זוגות סוגריים.
  • (...): יורט מהשורש של app/. שימושי כשהמודאל שלכם צריך לתפוס route רחוק בעץ.

הרמות נספרות ביחס למיקום התיקייה של ה-intercepting route בעץ הקבצים, לא ביחס למבנה ה-URL. אם יש לכם route group כמו (marketing) באמצע הדרך, הוא נספר כרמה גם אם הוא לא מופיע ב-URL. זו שגיאה נפוצה שגורמת ל-intercepting route לא לעבוד כפי שציפיתם, וקל לזהות אותה עם next dev --turbopack שמתריע על כך בקונסולה.

הדוגמה הקנונית לשילוב של parallel plus intercepting היא מודאל של גלריית תמונות עם URL ייחודי לכל תמונה. המשתמש רואה רשת של תמונות, לוחץ על אחת, ומקבל מודאל מעל הרשת. שיתוף הקישור מציג את העמוד המלא של אותה תמונה. הבסיס הוא שני נתיבים: /photos/[id] (העמוד המלא) ו-/photos/@modal/(.)photo/[id] (המודאל שיורט את הניווט).

app/
└─ photos/
   ├─ layout.tsx           // מזין את @modal כפרופ
   ├─ page.tsx             // רשת התמונות
   ├─ [id]/page.tsx        // עמוד תמונה מלא
   └─ @modal/
      ├─ default.tsx       // מחזיר null
      └─ (.)photo/[id]/
         └─ page.tsx       // המודאל

ה-layout.tsx של photos מזין את המודאל לצד ה-children:

// app/photos/layout.tsx
export default function PhotosLayout({
  children,
  modal,
}: {
  children: React.ReactNode
  modal: React.ReactNode
}) {
  return (
    <>
      {children}
      {modal}
    </>
  )
}

// app/photos/@modal/default.tsx
export default function Default() {
  return null
}

// app/photos/@modal/(.)photo/[id]/page.tsx
import { Modal } from '@/components/Modal'
import { getPhoto } from '@/lib/photos'

export default async function PhotoModal({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const photo = await getPhoto(id)
  return (
    <Modal>
      <img src={photo.url} alt={photo.alt} />
    </Modal>
  )
}

ב-Next.js 16 ה-params הן Promise ולכן צריך await, שינוי שהוכנס לצורך תמיכה מלאה ב-async rendering של React 19. אם אתם מגיעים מ-Next.js 14 או מוקדם יותר, כדאי לראות את המדריך שלנו על מעבר מ-middleware.ts ל-proxy.ts ב-Next.js 16 להבנה של שינויים דומים ב-API של App Router. הפריטים בתוך <Modal> יכולים בעצמם להיות server components, מה שאומר data fetching, אימות ו-DB queries קורים בשרת לפני שהמודאל מגיע ללקוח.

Conditional Routes ו-slots דינמיים

אחד השימושים העוצמתיים ביותר של parallel routes הוא רינדור מותנה של אזורים לפי הרשאות או מצב משתמש. אפשר להזרים ב-layout.tsx את התוצאה של בדיקת session ולהחליט אילו slots לרנדר. הדוגמה הבאה מציגה dashboard שמפצל לאזור admin ואזור user, כאשר רק אחד מהם מוצג בפועל:

// app/dashboard/layout.tsx
import { auth } from '@/lib/auth'

export default async function Layout({
  admin,
  user,
}: {
  admin: React.ReactNode
  user: React.ReactNode
}) {
  const session = await auth()
  return session?.user.role === 'admin' ? admin : user
}

מכיוון שכל slot הוא tree עצמאי, ה-admin וה-user יכולים להיות בעלי layouts, loading states ו-error boundaries משלהם, בלי צורך בקוד תנאי בתוך הקומפוננטים. הדפוס הזה משפר ביצועים באופן משמעותי: אם המשתמש לא admin, קוד ה-admin לא נכלל אפילו ב-server bundle, כי ה-tree שלו אף פעם לא נדרש. אני משלב את הגישה הזו לעיתים תכופות עם Server Actions ב-Next.js 16 כדי לבנות פורמים מתאימי-הרשאה בלי חיפוש-מסלולים ידני.

Loading ו-Error UI לכל slot

אחד היתרונות הכי גדולים של parallel routes הוא היכולת להגדיר loading.tsx ו-error.tsx לכל slot בנפרד. זה אומר ש-slot אחד יכול להיות במצב "טוען" בזמן שהאחרים כבר מוצגים במלואם, חוויה דומה לזו של partial hydration אבל בלי הסיבוכיות. כדי להגדיר את זה, פשוט הוסיפו loading.tsx בתוך תיקיית ה-slot:

// app/dashboard/@feed/loading.tsx
export default function FeedLoading() {
  return <div className="animate-pulse h-40 bg-muted rounded"/>
}

// app/dashboard/@feed/error.tsx
'use client'
export default function FeedError({ reset }: { reset: () => void }) {
  return (
    <div className="p-4 border border-destructive">
      <p>Feed failed to load.</p>
      <button onClick={reset}>Retry</button>
    </div>
  )
}

המנגנון עובד יד ביד עם Streaming ו-Suspense ב-Next.js 16: כל slot נעטף ב-<Suspense> נפרד, ולכן שגיאה או המתנה ב-slot אחד לא חוסמים את השאר. זה שינוי חשוב לעומת Next.js 12/13, שם boundary אחד יכול היה לחסום את כל העמוד. ב-release notes של Next.js 16 ב-GitHub תמצאו את הפירוט המלא של השינויים ב-Suspense boundary defaults.

TypeScript ו-slots: הזרקת טיפוסים ל-layout props

ב-TypeScript, כל slot הוא prop של React.ReactNode. אין generic חכם או type inference אוטומטי משם התיקייה, ואתם צריכים להצהיר על ה-slots ידנית ב-props של ה-layout. כדי לשמור על סנכרון בין קוד הקבצים לסוגי ה-props, אני ממליץ על שתי טכניקות:

  1. הגדירו טיפוס משותף type SlotProps ב-types/routing.ts ליציבות בעריכה.
  2. הפעילו את typedRoutes: true ב-next.config.ts כדי לקבל type checking על מסלולים בקישורים. זה תופס טעויות כשמישהו משנה שם של slot אבל שוכח לעדכן את המקומות שקוראים אותו.
// next.config.ts
import type { NextConfig } from 'next'

const config: NextConfig = {
  typedRoutes: true,
  experimental: {
    // הפעילו רק במידת הצורך; לרוב לא נדרש ב-16
  },
}
export default config

Slots לא מופיעים ב-URL, ולכן הם לא צריכים לעמוד במגבלות שמות של segments דינמיים. אבל כן חשוב להימנע משמות שמתחילים ב-@ בכל תיקייה שאינה slot: Next.js יזרוק שגיאת build אם היא לא מוזנת ל-layout כפרופ. חוץ מזה, ה-linter של Next.js 16 (שפועל אוטומטית עם Turbopack) מזהיר כשמצאתם slot ללא default.tsx, אזהרה שכדאי להתייחס אליה כאל שגיאה בכל CI. ראו גם את המדריך שלנו על Route Handlers ב-Next.js 16 אם המודאל שלכם צריך לתקשר עם API endpoints ייעודיים.

שאלות נפוצות

האם Parallel Routes עובדים עם React Server Components?

כן, במלואם. כל slot יכול להיות server component עם data fetching אסינכרוני, ו-Next.js משתמשת ב-streaming כדי לשלוח כל slot ברגע שהוא מוכן. גם שילוב של server ו-client components בתוך אותו slot הוא לגיטימי, ואין הבדל התנהגותי לעומת עמוד רגיל.

מה קורה אם שני parallel routes מפרסמים את אותו slug?

אין קונפליקט. ה-router מבחין ביניהם לפי שם התיקייה של ה-slot. אם @feed/[id] ו-@sidebar/[id] מוגדרים שניהם, ה-URL /dashboard/abc יגרום לכל אחד לרנדר את [id]/page.tsx שלו בהתאמה, ושניהם יקבלו את אותו params.id.

האם intercepting routes עובדים גם ל-back navigation?

כן. כשהמשתמש לוחץ back בדפדפן לאחר שנכנס למודאל דרך intercepting route, ה-URL חוזר לקודמו וה-slot של המודאל חוזר ל-default.tsx שלו, כלומר המודאל נסגר. זו הסיבה ש-router.back() היא הדרך הנכונה לסגור מודאל, לא state מקומי.

מתי כדאי להשתמש ב-Parallel Routes ומתי בקומפוננטים רגילים?

השתמשו ב-parallel routes כאשר אזורים שונים במסך צריכים ניווט עצמאי, מצבי טעינה נפרדים או data fetching אסינכרוני שלא צריך לחסום את שאר העמוד. לתצוגות סטטיות שנטענות פעם אחת עם ה-page, קומפוננטים רגילים פשוטים יותר ובעלי overhead נמוך יותר.

האם אפשר לקנן parallel routes בתוך parallel routes?

כן, קינון נתמך. כל slot יכול להכיל layout.tsx עם ה-slots שלו. תשומת לב אחת: default.tsx נדרש בכל רמת קינון, כי כל layout מנוהל בנפרד על ידי ה-router. במבנים עמוקים אני ממליץ ליצור טיפוסי props משותפים כדי למנוע חוסר סנכרון בין הרמות.

Editorial Team
אודות הכותב Editorial Team

Our team of expert writers and editors.