Intercepting Route — (.) (..) — modal pattern

~20 daq · Next.js · Routing Chuqurroq

Muammodan boshlaylik

Instagram da foto lentasini ko'ryapsiz. Bitta fotoga bosasiz — modal ochiladi, URL /p/abc123 ga o'zgaradi. Orqaga bosdingiz — lenta qaytadi, modal yopiladi.

Lekin /p/abc123 URL ini to'g'ridan-to'g'ri brauzerga yozsangiz — to'liq foto sahifasi ochiladi, lenta yo'q.

Bir xil URL — ikki xil ko'rinish:

  • Lentadan bosilganda → modal
  • To'g'ridan-to'g'ri kirilganda → to'liq sahifa

Bu — Intercepting Route.


Qanday ishlaydi

Intercepting Route — bir route boshqa route ni "to'xtatib" o'z ichida ko'rsatadi.

Konventsiya:

(.)   → bir xil darajadagi route ni to'xtatish
(..)  → bir daraja yuqoridagi route ni to'xtatish
(..)(..) → ikki daraja yuqori
(...)  → root dan

Amaliy misol — foto modal

Instagram kabi: foto ro'yxatidan bosib modal, URL dan kirsa to'liq sahifa.

app/
├── photos/
│   ├── page.tsx              →  /photos (ro'yxat)
│   ├── [id]/
│   │   └── page.tsx          →  /photos/42 (to'liq sahifa)
│   └── (.)photos/            ← intercepting!
│       └── [id]/
│           └── page.tsx      →  /photos/42 (modal sifatida)

(.)photos — "bir xil darajadagi photos route ni to'xtat" degani.


To'liq implementatsiya

// app/photos/page.tsx — foto ro'yxati
import Link from 'next/link'

export default async function PhotosPage() {
  const photos = await getPhotos()

  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '8px' }}>
      {photos.map(photo => (
        <Link key={photo.id} href={`/photos/${photo.id}`}>
          <img src={photo.thumbnail} alt={photo.title} />
        </Link>
      ))}
    </div>
  )
}
// app/photos/[id]/page.tsx — to'liq sahifa (URL dan to'g'ridan-to'g'ri kirilganda)
export default async function PhotoPage({ params }) {
  const { id } = await params
  const photo = await getPhoto(id)

  return (
    <div>
      <img src={photo.url} alt={photo.title} />
      <h1>{photo.title}</h1>
      <p>{photo.description}</p>
    </div>
  )
}
// app/photos/(.)photos/[id]/page.tsx — modal (ro'yxatdan bosilganda)
'use client'

import { useRouter } from 'next/navigation'
import { useEffect } from 'react'

export default function PhotoModal({ params }) {
  const router = useRouter()
  const { id } = params

  // ESC bosilganda yopish
  useEffect(() => {
    function handleKey(e: KeyboardEvent) {
      if (e.key === 'Escape') router.back()
    }
    window.addEventListener('keydown', handleKey)
    return () => window.removeEventListener('keydown', handleKey)
  }, [router])

  return (
    <div
      style={{
        position: 'fixed', inset: 0,
        background: 'rgba(0,0,0,0.8)',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        zIndex: 50,
      }}
      onClick={() => router.back()}  // tashqariga bosib yopish
    >
      <div onClick={e => e.stopPropagation()}>
        <PhotoContent id={id} />
      </div>
    </div>
  )
}

Parallel Route bilan birga — to'liq modal pattern

Intercepting Route ko'pincha Parallel Route bilan birga ishlatiladi. Modal layout da @modal slot sifatida ko'rsatiladi:

app/
├── layout.tsx                ← @modal slot qabul qiladi
├── photos/
│   ├── page.tsx
│   └── [id]/
│       └── page.tsx
└── @modal/
    └── (.)photos/
        └── [id]/
            └── page.tsx      ← modal
// app/layout.tsx
export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode
  modal: React.ReactNode
}) {
  return (
    <html>
      <body>
        {children}
        {modal}  {/* modal shu yerda ko'rsatiladi */}
      </body>
    </html>
  )
}
// app/@modal/default.tsx — modal yo'q bo'lganda
export default function ModalDefault() {
  return null
}

Endi:

  • /photos ga kirganda — children = foto ro'yxati, modal = null
  • Fotoga bosilganda — URL /photos/42, children = ro'yxat (o'zgarmaydi!), modal = modal
  • To'g'ridan-to'g'ri /photos/42 ga kirilganda — children = to'liq sahifa, modal = null

(.) konventsiyasi — qaysi daraja

app/
├── feed/
│   └── page.tsx
└── photos/
    ├── page.tsx
    ├── [id]/
    │   └── page.tsx
    └── (.)photos/      ← photos/ bilan bir xil daraja → (.)
        └── [id]/
            └── page.tsx

Agar intercepting route bir daraja yuqorida bo'lsa:

app/
├── photos/
│   └── [id]/
│       └── page.tsx
└── feed/
    └── (..)photos/     ← bir daraja yuqori → (..)
        └── [id]/
            └── page.tsx

Tez-tez uchraydigan xatolar

1. default.tsx yo'q — navigatsiyada xato

// ❌ — @modal slot uchun default.tsx yo'q
// Boshqa sahifaga o'tganda modal slot 404

// ✅
app/@modal/default.tsx  → null qaytaradi

2. Daraja konventsiyasini noto'g'ri tanlash

// app/photos/(.)photos/[id] — to'g'ri (bir xil daraja)
// app/photos/(..)photos/[id] — xato (bir daraja yuqori emas)

Papkalar joylashuviga qarab (.), (..), (...) tanlanadi.

3. Modal ichida router.back() o'rniga router.push() ishlatish

// ❌ — modal yopilganda ro'yxat yo'qoladi
onClick={() => router.push('/photos')}

// ✅ — orqaga qaytish, ro'yxat saqlanadi
onClick={() => router.back()}

Xulosa

Intercepting Route — bir xil URL, ikki xil ko'rinish.

Ro'yxatdan bosilganda  → modal (intercepting route)
URL dan to'g'ridan-to'g'ri → to'liq sahifa

(.)    → bir xil daraja
(..)   → bir daraja yuqori
(...)  → root dan

Parallel Route (@modal) + Intercepting Route → to'liq modal pattern

Keyingi dars

28-dars: Middleware — request kelganda ishlaydigan kod, matcher

Har bir so'rov kelganda — autentifikatsiya tekshiruvi, redirect, header qo'shish — middleware bilan.