Parallel Routes + Intercepting Routes ใน Next.js 16: คู่มือสร้าง Modal แบบ Deep Link ฉบับสมบูรณ์

เรียนรู้วิธีสร้าง Modal ที่ Deep Link ได้ แชร์ URL ได้ ด้วย Parallel Routes และ Intercepting Routes ใน Next.js 16 พร้อมตัวอย่างโค้ด Gallery Modal, Login Modal และ best practices ที่ต้องรู้

ทำไม Modal ธรรมดาถึงไม่ตอบโจทย์อีกต่อไป

ถ้าคุณเคยสร้าง Modal ในเว็บแอปมาก่อน น่าจะเคยเจอปัญหาเหล่านี้แน่ๆ — ผู้ใช้เปิด Modal ดูรายละเอียดสินค้า แล้วกด refresh หน้า Modal หายไปเลย หรือลองก๊อปปี้ URL ส่งให้เพื่อน พอเพื่อนเปิดลิงก์ กลับไม่เห็น Modal เลย ได้แค่หน้าหลักเฉยๆ

หรือแม้แต่เรื่องง่ายๆ อย่างการกดปุ่ม Back ของเบราว์เซอร์ แทนที่จะปิด Modal กลับพาไปหน้าก่อนหน้าเลย น่าหงุดหงิดมาก

ปัญหาเหล่านี้เกิดจากการที่ Modal แบบเดิมใช้ state ของ component ในการควบคุมการเปิด-ปิด ซึ่งไม่ได้ผูกกับ URL เลย พอ URL ไม่เปลี่ยน ก็แชร์ได้ไม่สะดวก bookmark ไม่ได้ และ browser navigation ก็ทำงานไม่ถูกต้อง

Next.js App Router มีทางออกที่สวยงามสำหรับปัญหานี้ ด้วยการรวมพลังของ 2 ฟีเจอร์ — Parallel Routes กับ Intercepting Routes ซึ่งเปลี่ยน Modal ธรรมดาให้กลายเป็น Modal ที่ผูกกับ URL, แชร์ได้, deep link ได้ และทำงานร่วมกับ browser navigation ได้อย่างสมบูรณ์แบบ มาดูกันว่ามันทำงานยังไง

Parallel Routes คืออะไร? ทำความรู้จัก Named Slots

Parallel Routes คือความสามารถของ Next.js ที่ให้คุณ render หลายหน้า (หรือ UI หลายชิ้น) พร้อมกันภายใน layout เดียวกัน โดยใช้สิ่งที่เรียกว่า Named Slots

พูดง่ายๆ ก็คือ คุณสร้างโฟลเดอร์ที่ขึ้นต้นด้วย @ เช่น @modal, @sidebar, @dashboard แล้ว slot เหล่านี้จะถูกส่งเป็น props ให้กับ layout ที่เป็น parent โดยอัตโนมัติ

ลองดูตัวอย่างโครงสร้างไฟล์กัน:

app/
├── layout.tsx          # ← รับ @modal เป็น prop
├── page.tsx
├── @modal/
│   ├── default.tsx     # ← return null (สำคัญมาก!)
│   └── (.)product/[id]/
│       └── page.tsx    # ← Modal content
└── product/[id]/
    └── page.tsx        # ← Full page (เมื่อเข้าตรง)

จากนั้นใน layout.tsx เราก็รับ slot เป็น prop ได้แบบนี้:

// app/layout.tsx
export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode
  modal: React.ReactNode
}) {
  return (
    <html lang="th">
      <body>
        {children}
        {modal}
      </body>
    </html>
  )
}

จุดสำคัญที่ต้องจำเกี่ยวกับ Parallel Routes

  • Slot ไม่ใช่ route segment — ไม่ส่งผลต่อ URL ดังนั้น @modal จะไม่ปรากฏใน URL bar
  • Slot ทำงานได้เฉพาะใน layout.tsx เท่านั้น ใช้ใน page.tsx ไม่ได้นะ
  • แต่ละ slot สามารถมี loading.tsx และ error.tsx แยกกันได้ ทำให้ stream ได้อย่างอิสระ
  • ต้องมี default.tsx เสมอ ไม่งั้น Next.js จะแสดง 404 เมื่อ slot ไม่ตรงกับ route ปัจจุบัน (เจ็บมาแล้ว)

Intercepting Routes คืออะไร? หลักการดักจับเส้นทาง

Intercepting Routes คือกลไกที่ให้คุณ "ดักจับ" การ navigate ไปยัง route อื่น แล้วแสดงเนื้อหาของ route นั้นภายใน layout ปัจจุบัน แทนที่จะ navigate ออกไปจริงๆ

ลองนึกภาพตามนะ — ผู้ใช้อยู่ในหน้ารายการสินค้า กดคลิกสินค้าชิ้นหนึ่ง แทนที่จะไปหน้าสินค้าเต็มๆ Next.js จะ intercept การ navigate นั้นแล้วเปิด Modal แสดงรายละเอียดสินค้าซ้อนทับบนหน้าเดิม ผู้ใช้ไม่เสีย context เลย เจ๋งมากจริงๆ

การตั้งชื่อโฟลเดอร์สำหรับ Intercepting Routes ใช้รูปแบบ (..) ซึ่งคล้ายกับ relative path ที่เราคุ้นเคย:

  • (.) — intercept route ที่อยู่ ระดับเดียวกัน
  • (..) — intercept route ที่อยู่ ระดับบน 1 ขั้น
  • (..)(..) — intercept route ที่อยู่ ระดับบน 2 ขั้น
  • (...) — intercept route จาก root app directory

ข้อสำคัญ: convention (..) นี้ทำงานตาม route segments ไม่ใช่ file system นะ ดังนั้นโฟลเดอร์ @slot จะไม่ถูกนับเป็น segment ตรงนี้หลายคนพลาด

รวมพลัง: สร้าง Modal ที่ Deep Link ได้จริง

ทีนี้พอเอา Parallel Routes กับ Intercepting Routes มารวมกัน เราก็จะได้ Modal ที่แก้ปัญหาทั้งหมดที่กล่าวมา:

  • Modal มี URL เป็นของตัวเอง — แชร์ได้ผ่าน link
  • Refresh หน้าแล้ว Modal ไม่หาย — ไปแสดงเป็น full page แทน
  • กดปุ่ม Back จะปิด Modal ไม่ใช่ย้อนกลับไปหน้าอื่น
  • กดปุ่ม Forward จะเปิด Modal กลับมา

โอเค มาลงมือสร้างกันเลย — เราจะทำหน้า Gallery ที่คลิกรูปภาพแล้วเปิด Modal แสดงรูปขยายได้

ขั้นตอนที่ 1: สร้างโครงสร้างไฟล์

app/
├── layout.tsx
├── page.tsx                    # หน้า Gallery
├── @modal/
│   ├── default.tsx             # return null
│   ├── (..)photo/[id]/
│   │   └── page.tsx            # Modal แสดงรูปภาพ
│   └── [...catchAll]/
│       └── page.tsx            # Catch-all return null
└── photo/[id]/
    └── page.tsx                # หน้ารูปภาพเต็ม (direct access)

ขั้นตอนที่ 2: สร้าง Root Layout รับ @modal Slot

// app/layout.tsx
import './globals.css'

export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode
  modal: React.ReactNode
}) {
  return (
    <html lang="th">
      <body>
        <nav className="p-4 border-b">
          <a href="/">Gallery App</a>
        </nav>
        {children}
        {modal}
      </body>
    </html>
  )
}

ขั้นตอนที่ 3: สร้างหน้า Gallery

// app/page.tsx
import Link from 'next/link'

const photos = [
  { id: 1, title: 'ภูเขาฟูจิ', src: '/photos/fuji.jpg' },
  { id: 2, title: 'ทะเลอันดามัน', src: '/photos/andaman.jpg' },
  { id: 3, title: 'ดอยอินทนนท์', src: '/photos/inthanon.jpg' },
]

export default function GalleryPage() {
  return (
    <main className="p-8">
      <h1 className="text-3xl font-bold mb-6">แกลเลอรี่ภาพถ่าย</h1>
      <div className="grid grid-cols-3 gap-4">
        {photos.map((photo) => (
          <Link
            key={photo.id}
            href={`/photo/${photo.id}`}
            className="block overflow-hidden rounded-lg"
          >
            <img
              src={photo.src}
              alt={photo.title}
              className="w-full h-48 object-cover hover:scale-105 transition"
            />
            <p className="mt-2 text-sm">{photo.title}</p>
          </Link>
        ))}
      </div>
    </main>
  )
}

สังเกตว่าเราใช้ <Link> จาก Next.js นะ ไม่ใช่ <a> ธรรมดา — ตรงนี้สำคัญมาก เพราะ Intercepting Routes จะทำงานเฉพาะกับ client-side navigation ที่ผ่าน <Link> เท่านั้น ถ้าพลาดตรงนี้ นั่งดีบักยาวเลย

ขั้นตอนที่ 4: สร้าง Modal Component

// components/Modal.tsx
'use client'

import { useRouter } from 'next/navigation'
import { useCallback, useEffect, useRef } from 'react'

export function Modal({ children }: { children: React.ReactNode }) {
  const router = useRouter()
  const overlayRef = useRef<HTMLDivElement>(null)

  const onDismiss = useCallback(() => {
    router.back()
  }, [router])

  const onKeyDown = useCallback(
    (e: KeyboardEvent) => {
      if (e.key === 'Escape') onDismiss()
    },
    [onDismiss]
  )

  useEffect(() => {
    document.addEventListener('keydown', onKeyDown)
    return () => document.removeEventListener('keydown', onKeyDown)
  }, [onKeyDown])

  return (
    <div
      ref={overlayRef}
      className="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
      onClick={(e) => {
        if (e.target === overlayRef.current) onDismiss()
      }}
    >
      <div className="relative bg-white rounded-xl max-w-2xl w-full mx-4 p-6">
        <button
          onClick={onDismiss}
          className="absolute top-3 right-3 text-gray-500 hover:text-gray-800"
          aria-label="ปิด"
        >
          ✕
        </button>
        {children}
      </div>
    </div>
  )
}

Modal component ตัวนี้จัดการได้ครบถ้วน:

  • กด Escape ปิด Modal ได้
  • คลิก overlay ด้านนอก ปิด Modal ได้
  • ใช้ router.back() ในการปิด Modal เพื่อให้ URL ย้อนกลับถูกต้อง

ตรงที่ใช้ router.back() แทน router.push('/') นี่สำคัญนะ เพราะมันทำให้ browser history ทำงานได้ถูกต้องตาม flow

ขั้นตอนที่ 5: สร้าง Intercepted Route (Modal Content)

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

const photos: Record<string, { title: string; src: string; desc: string }> = {
  '1': { title: 'ภูเขาฟูจิ', src: '/photos/fuji.jpg', desc: 'ภูเขาไฟที่มีชื่อเสียงที่สุดของญี่ปุ่น' },
  '2': { title: 'ทะเลอันดามัน', src: '/photos/andaman.jpg', desc: 'ทะเลฝั่งตะวันตกของภาคใต้ไทย' },
  '3': { title: 'ดอยอินทนนท์', src: '/photos/inthanon.jpg', desc: 'ยอดเขาที่สูงที่สุดในประเทศไทย' },
}

export default async function PhotoModal({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const photo = photos[id]
  if (!photo) return null

  return (
    <Modal>
      <img
        src={photo.src}
        alt={photo.title}
        className="w-full rounded-lg mb-4"
      />
      <h2 className="text-xl font-bold">{photo.title}</h2>
      <p className="text-gray-600 mt-2">{photo.desc}</p>
    </Modal>
  )
}

นี่คือหัวใจของระบบเลย — เมื่อผู้ใช้คลิก <Link href="/photo/1"> จากหน้า Gallery ไฟล์นี้จะถูก render แทนที่ /photo/[id]/page.tsx เพราะ (..)photo intercept route ที่อยู่ระดับบน 1 ขั้น (จำได้ไหม @modal ไม่ถูกนับเป็น segment)

ขั้นตอนที่ 6: สร้าง Default และ Catch-All สำหรับ @modal Slot

// app/@modal/default.tsx
export default function Default() {
  return null
}
// app/@modal/[...catchAll]/page.tsx
export default function CatchAll() {
  return null
}

default.tsx สำคัญมากจริงๆ — ถ้าไม่มีไฟล์นี้ Next.js จะแสดง 404 เมื่อ slot ไม่ตรงกับ route ปัจจุบัน ส่วน catch-all route ช่วยให้ Modal ปิดได้อย่างถูกต้องเมื่อ navigate ไปยัง route อื่นๆ ลืมใส่ไฟล์นี้ทีนึงนั่งหาบั๊กอยู่นานเลย

ขั้นตอนที่ 7: สร้างหน้า Full Page สำหรับ Direct Access

// app/photo/[id]/page.tsx
import Link from 'next/link'

const photos: Record<string, { title: string; src: string; desc: string }> = {
  '1': { title: 'ภูเขาฟูจิ', src: '/photos/fuji.jpg', desc: 'ภูเขาไฟที่มีชื่อเสียงที่สุดของญี่ปุ่น' },
  '2': { title: 'ทะเลอันดามัน', src: '/photos/andaman.jpg', desc: 'ทะเลฝั่งตะวันตกของภาคใต้ไทย' },
  '3': { title: 'ดอยอินทนนท์', src: '/photos/inthanon.jpg', desc: 'ยอดเขาที่สูงที่สุดในประเทศไทย' },
}

export default async function PhotoPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const photo = photos[id]
  if (!photo) return <p>ไม่พบรูปภาพ</p>

  return (
    <main className="max-w-3xl mx-auto p-8">
      <Link href="/" className="text-blue-600 hover:underline mb-4 block">
        ← กลับไปหน้า Gallery
      </Link>
      <img
        src={photo.src}
        alt={photo.title}
        className="w-full rounded-xl mb-6"
      />
      <h1 className="text-3xl font-bold">{photo.title}</h1>
      <p className="text-gray-600 mt-4 text-lg">{photo.desc}</p>
    </main>
  )
}

หน้านี้จะแสดงเมื่อผู้ใช้เข้าถึง URL /photo/1 โดยตรง เช่น พิมพ์ URL ในเบราว์เซอร์ กด refresh หรือเปิดจาก link ที่แชร์มา ผลลัพธ์คือได้ประสบการณ์ที่ดีทั้ง 2 แบบ — Modal สำหรับ browse อย่างรวดเร็ว และ full page สำหรับดูรายละเอียดเต็ม

Flow การทำงาน: Soft Navigation vs Hard Navigation

ทำความเข้าใจตรงนี้ให้ดีนะ เพราะมันเป็นกุญแจสำคัญที่อธิบายว่าทำไม pattern นี้ถึงทำงานได้

Soft Navigation (Client-Side)

เกิดขึ้นเมื่อผู้ใช้คลิก <Link> — Next.js ทำ partial render โดยเปลี่ยนเฉพาะ slot ที่ตรงกับ route ใหม่ ส่วน slot อื่นๆ ยังคงแสดง content เดิม

  1. ผู้ใช้อยู่ที่หน้า / (Gallery)
  2. คลิก <Link href="/photo/1">
  3. Next.js intercept navigation — render @modal/(..)photo/[id]/page.tsx แทน
  4. URL เปลี่ยนเป็น /photo/1 แต่หน้า Gallery ยังแสดงอยู่เบื้องหลัง
  5. Modal ซ้อนทับหน้า Gallery — สวยงาม!

Hard Navigation (Browser Refresh / Direct URL)

เกิดขึ้นเมื่อผู้ใช้ refresh หน้าหรือเข้าตรง — Next.js ไม่สามารถระบุ active state ของ slot ได้ จึงแสดง default.tsx สำหรับ @modal (ซึ่ง return null) และแสดง /photo/[id]/page.tsx เป็น full page

  1. ผู้ใช้เข้า URL /photo/1 โดยตรง
  2. Next.js render /photo/[id]/page.tsx เป็นหน้าเต็ม
  3. @modal/default.tsx return null — ไม่มี Modal แสดง
  4. ผู้ใช้เห็นหน้ารูปภาพเต็มรูปแบบ

เห็นไหมว่า URL เดียวกัน แต่แสดงผลได้ 2 แบบ ขึ้นอยู่กับว่าเข้ามายังไง นี่แหละที่ทำให้ pattern นี้ทรงพลังมาก

ตัวอย่างที่ 2: Login Modal ที่แชร์ URL ได้

อีกตัวอย่างที่พบบ่อยมากคือ Login Modal — ผู้ใช้คลิกปุ่ม "เข้าสู่ระบบ" แล้วเปิด Modal ขึ้นมาแทนที่จะไปหน้า /login แต่ถ้าเปิด /login ตรงก็จะเห็นหน้า Login เต็ม

โครงสร้างไฟล์

app/
├── layout.tsx
├── page.tsx
├── @auth/
│   ├── default.tsx
│   ├── (.)login/
│   │   └── page.tsx        # Login Modal
│   └── [...catchAll]/
│       └── page.tsx
└── login/
    └── page.tsx             # Login Full Page

Layout ที่รับ @auth Slot

// app/layout.tsx
import Link from 'next/link'

export default function RootLayout({
  children,
  auth,
}: {
  children: React.ReactNode
  auth: React.ReactNode
}) {
  return (
    <html lang="th">
      <body>
        <nav className="flex justify-between p-4 border-b">
          <Link href="/">หน้าแรก</Link>
          <Link href="/login">เข้าสู่ระบบ</Link>
        </nav>
        {children}
        {auth}
      </body>
    </html>
  )
}

Intercepted Login Modal

// app/@auth/(.)login/page.tsx
import { Modal } from '@/components/Modal'

export default function LoginModal() {
  return (
    <Modal>
      <h2 className="text-xl font-bold mb-4">เข้าสู่ระบบ</h2>
      <form className="space-y-4">
        <div>
          <label className="block text-sm font-medium">อีเมล</label>
          <input
            type="email"
            className="w-full border rounded-lg p-2 mt-1"
            placeholder="[email protected]"
          />
        </div>
        <div>
          <label className="block text-sm font-medium">รหัสผ่าน</label>
          <input
            type="password"
            className="w-full border rounded-lg p-2 mt-1"
          />
        </div>
        <button
          type="submit"
          className="w-full bg-blue-600 text-white py-2 rounded-lg"
        >
          เข้าสู่ระบบ
        </button>
      </form>
    </Modal>
  )
}

Full Login Page

// app/login/page.tsx
export default function LoginPage() {
  return (
    <main className="max-w-md mx-auto mt-20 p-8 border rounded-xl">
      <h1 className="text-2xl font-bold mb-6 text-center">เข้าสู่ระบบ</h1>
      <form className="space-y-4">
        <div>
          <label className="block text-sm font-medium">อีเมล</label>
          <input
            type="email"
            className="w-full border rounded-lg p-2 mt-1"
            placeholder="[email protected]"
          />
        </div>
        <div>
          <label className="block text-sm font-medium">รหัสผ่าน</label>
          <input
            type="password"
            className="w-full border rounded-lg p-2 mt-1"
          />
        </div>
        <button
          type="submit"
          className="w-full bg-blue-600 text-white py-2 rounded-lg"
        >
          เข้าสู่ระบบ
        </button>
      </form>
    </main>
  )
}

ผลลัพธ์ก็เหมือนกัน — เมื่อผู้ใช้คลิก "เข้าสู่ระบบ" จาก navbar จะเห็น Login Modal ซ้อนทับหน้าเดิม แต่ถ้าเปิด /login ตรงจะเห็นหน้า Login เต็ม ทั้ง 2 แบบใช้ URL เดียวกัน!

Best Practices: เคล็ดลับที่ควรรู้ก่อนใช้งานจริง

1. ใช้ <Link> เท่านั้น ห้ามใช้ <a>

Intercepting Routes ทำงานเฉพาะกับ client-side navigation ที่ผ่าน component <Link> ของ Next.js เท่านั้น ถ้าใช้ <a> ธรรมดา จะกลายเป็น hard navigation และข้ามการ intercept ไปเลย พูดตามตรงนี่คือจุดที่คนผิดพลาดบ่อยที่สุด

2. แยก Modal Shell ออกจาก Content

สร้าง Modal เป็น component แยกต่างหาก แล้วใส่ content เป็น children วิธีนี้ทำให้ content สามารถเป็น Server Component ได้ ซึ่งช่วยลด JavaScript ที่ต้องส่งไปฝั่ง client ได้เยอะเลย

3. รวม Intercept Routes ไว้ใน Parallel Route เดียว

ถ้าต้องการ Modal หลายแบบ (login, cart, photo) ให้รวมไว้ใน @modal slot เดียว อย่าสร้าง @login, @cart, @photo แยกกันนะ ไม่งั้น Modal หลายตัวอาจแสดงพร้อมกันได้

4. จัดการ Route Groups เพื่อจำกัดขอบเขต Modal

ถ้า Modal ไม่ควรแสดงในทุกหน้า ให้ใช้ Route Groups (group) เพื่อจำกัดขอบเขตว่า Modal จะ intercept ได้เฉพาะภายใน group ที่กำหนด

app/
├── (shop)/
│   ├── layout.tsx           # layout ที่รับ @modal
│   ├── @modal/
│   │   ├── default.tsx
│   │   └── (.)product/[id]/
│   │       └── page.tsx
│   ├── page.tsx             # หน้าร้านค้า
│   └── product/[id]/
│       └── page.tsx
└── (auth)/
    ├── layout.tsx           # layout แยกสำหรับ auth
    └── login/
        └── page.tsx

5. ป้องกัน Modal ค้างหลัง Navigate

เนื่องจาก layout ไม่ re-render เมื่อ navigate ภายในตัว ถ้า navigate กลับไปหน้า parent แต่ Modal ยังแสดงอยู่ ให้ใช้ usePathname() ตรวจสอบ pathname ปัจจุบัน แล้ว conditionally render Modal แบบนี้:

// components/ModalGuard.tsx
'use client'

import { usePathname } from 'next/navigation'

export function ModalGuard({
  expectedPath,
  children,
}: {
  expectedPath: string
  children: React.ReactNode
}) {
  const pathname = usePathname()
  if (!pathname.startsWith(expectedPath)) return null
  return <>{children}</>
}

6. อย่าลืมเรื่อง Accessibility

เรื่องนี้หลายคนมักจะข้ามไป แต่สำคัญมากนะ — ตั้ง role="dialog", aria-modal="true", trap focus ภายใน Modal, และรองรับ keyboard navigation ด้วย Escape key ผู้ใช้ที่ใช้ screen reader จะขอบคุณเรา

Conditional Rendering ด้วย Parallel Routes

นอกจาก Modal แล้ว Parallel Routes ยังเอาไปใช้ทำ conditional rendering ได้อีก เช่น แสดง Dashboard ต่างกันตามบทบาทของผู้ใช้:

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

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

  return (
    <main>
      {role === 'admin' ? admin : user}
    </main>
  )
}

pattern นี้ทำให้สามารถ render UI ที่แตกต่างกันตามเงื่อนไขได้อย่างง่ายดาย แต่ละ slot มี loading state, error boundary, และ data fetching เป็นของตัวเอง ไม่กระทบกัน

ข้อควรระวังและ Bug ที่รู้จักใน Next.js 16

ก่อนจะเอาไปใช้งานจริง มีเรื่องที่ควรรู้ไว้:

  • trailingSlash bug: มี bug ที่รู้จักใน Next.js 16 เมื่อตั้ง trailingSlash: true ใน next.config.ts ร่วมกับ Intercepting Routes จะทำให้ popup เปิดเป็น static page แทนที่จะเป็น modal ถ้าเจอปัญหานี้ ลองปิด trailingSlash ไปก่อนเป็น workaround
  • Multiple Parallel Routes: ถ้ามี parallel routes หลายตัวที่มี modal ซ้อนกัน อาจเกิดปัญหา modal หลายตัวแสดงพร้อมกัน ทางแก้คือรวม intercept routes ทั้งหมดไว้ใน parallel route ตัวเดียว
  • Link ข้าม Modal: ถ้าใน modal มี link ไปยัง modal อื่น modal ตัวแรกอาจยังค้างแสดงอยู่ ควรปิด modal ตัวเดิมก่อนค่อย navigate ไป modal ใหม่

คำถามที่พบบ่อย (FAQ)

Parallel Routes กับ Intercepting Routes ต่างกันอย่างไร?

Parallel Routes ให้คุณแสดง UI หลายชิ้นพร้อมกันใน layout เดียวผ่าน named slots (โฟลเดอร์ @) ส่วน Intercepting Routes ให้คุณดักจับ navigation แล้วแสดง content ของ route อื่นภายใน layout ปัจจุบัน (โฟลเดอร์ (..)) ทั้งสองมักใช้ร่วมกันเพื่อสร้าง modal ที่ผูกกับ URL

ทำไมต้องมีไฟล์ default.tsx ใน Parallel Route slot?

เมื่อเกิด hard navigation (refresh หรือเข้าตรง) Next.js จะพยายาม render ทุก slot ถ้า slot ไม่ตรงกับ route ปัจจุบันและไม่มี default.tsx ก็จะโดน 404 เลย ดังนั้น default.tsx ที่ return null จึงเป็นสิ่งจำเป็น มันบอก Next.js ว่า "ไม่ต้องแสดงอะไรสำหรับ slot นี้ก็ได้นะ"

Intercepting Routes ทำงานกับ Dynamic Routes ได้ไหม?

ได้เลย ใช้งานร่วมกับ dynamic segments [param] ได้ปกติ เช่น @modal/(.)product/[id]/page.tsx จะ intercept route /product/123 ได้ แต่อย่าลืมว่า convention (..) อิงตาม route segments ไม่ใช่ file system ดังนั้นโฟลเดอร์ @slot ไม่ถูกนับนะ

สามารถใช้ Server Actions ภายใน Modal ได้หรือไม่?

ได้แน่นอน เพราะ Modal content สามารถเป็น Server Component ได้ (เฉพาะ Modal shell ที่ต้องเป็น Client Component เพราะจัดการ events) คุณจึงใช้ Server Actions สำหรับ form submission ภายใน Modal ได้ตามปกติ ซึ่งช่วยลด JavaScript ที่ต้องส่งไป client ได้อีกด้วย

ปุ่ม Back ของเบราว์เซอร์จะปิด Modal ได้โดยอัตโนมัติไหม?

ได้ครับ! เพราะ Modal ถูกเปิดผ่าน client-side navigation ทำให้ URL เปลี่ยนจาก / เป็น /photo/1 เมื่อกดปุ่ม Back เบราว์เซอร์จะ navigate กลับไป / แล้ว Parallel Route ก็จะแสดง default.tsx (null) แทน Modal — เท่ากับปิด Modal โดยอัตโนมัติเลย ส่วนปุ่มปิดภายใน Modal เราก็ใช้ router.back() เพื่อให้ทำงานเหมือนกัน

เกี่ยวกับผู้เขียน Editorial Team

Our team of expert writers and editors.