ข้ามไปยังเนื้อหา

Linking และ Navigation

<Link> จัดการ navigation ส่วนใหญ่ด้วย client-side transition และ prefetch อัตโนมัติ ส่วน useRouter และ helper จาก next/navigation ครอบคลุม navigation แบบ programmatic และฝั่ง server

<Link> navigate ฝั่ง client โดยไม่ reload หน้าใหม่ทั้งหน้า มันขยายมาจาก native <a> element เลยใช้กับ URL ภายนอกธรรมดาได้ด้วย — แต่ prefetch กับ client-side transition ใช้ได้แค่กับ route ภายใน app ตัวเองเท่านั้น

import Link from 'next/link'
export default function Nav() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/blog/42">Post 42</Link>
<Link href="/dashboard" prefetch={false}>Dashboard</Link>
<Link href="https://nextjs.org">External site (plain anchor behavior)</Link>
</nav>
)
}

ณ Next.js 16.3.6 prop prefetch ของ App Router รับได้แค่ 3 ค่านี้เท่านั้น (ตรวจกับ docs ที่ bundle มากับเวอร์ชันนี้แล้ว)

ค่าพฤติกรรม
"auto" หรือ null (default)route แบบ static prefetch เต็ม ๆ ส่วน route แบบ dynamic prefetch แค่ถึง loading.js boundary ที่ใกล้ที่สุด
trueprefetch route เต็ม ๆ เสมอไม่ว่า static หรือ dynamic — ถ้าเปิด Partial Prefetching ไว้จะรวม App Shell กับ cached content ที่ขึ้นกับ URL ด้วย
falseไม่ prefetch เลย — ไม่ทั้งตอนเข้า viewport และไม่ทั้งตอน hover

route ที่ prefetch แล้วจะเข้าไปอยู่ใน Client Cache การ navigate จึงอาจถูก serve ทันทีจาก RSC payload ที่ cache ไว้แทนที่จะยิง request ใหม่ เรื่อง Client Cache เต็ม ๆ อยู่ใน module Data Fetching

สำหรับ navigation ที่ trigger ด้วย logic เช่นหลัง submit form หลัง timeout หรือหลังเช็ค auth ให้ใช้ useRouter จาก next/navigation ใน Client Component

'use client'
import { useRouter } from 'next/navigation'
export default function SaveButton() {
const router = useRouter()
async function onSave() {
await fetch('/api/save', { method: 'POST' })
router.push('/dashboard') // or replace, back, refresh
}
return <button onClick={onSave}>Save</button>
}

router.push เพิ่ม history entry, router.replace แทนที่ entry ปัจจุบัน, router.back ย้อนกลับ, ส่วน router.refresh fetch ข้อมูลใหม่แล้ว re-render Server Component ของ route ปัจจุบัน — มัน clear Client Cache ของ route นั้น แต่ไม่ invalidate server-side cache ใด ๆ เลย (ใช้ revalidatePath/revalidateTag สำหรับฝั่ง server แทน)

Client Component อ่าน navigation state ด้วย hook จาก next/navigation ได้แก่ usePathname, useSearchParams และ useParams

'use client'
import { usePathname, useSearchParams, useParams } from 'next/navigation'
export default function Debug() {
const pathname = usePathname() // "/blog/42"
const searchParams = useSearchParams() // read ?sort=new
const params = useParams() // { id: "42" }
return <pre>{pathname} {searchParams.get('sort')} {params.id}</pre>
}

redirect() กับ permanentRedirect() (ทั้งคู่มาจาก next/navigation) ทำงานด้วยการ throw string digest ของ error ที่ throw ออกมาเข้ารหัสปลายทางกับ status ไว้ในตัวข้อความเลย นี่คือวิธีที่ Next.js จำมันได้ตอนไล่กลับขึ้นไปบน call stack แล้วแปลงเป็น HTTP redirect จริง ๆ แทนที่จะเป็น crash การครอบ call นั้นด้วย try/catch ของเราเองจะดักมันไว้ก่อน Next.js จะเห็น — redirect หายไปเงียบ ๆ

นี่คือ bug รูปแบบเดียวกับที่ module มุตเมชันพิสูจน์ไว้สำหรับ notFound() ข้างใน Server Action (ดู lesson mutations-and-revalidation ของ server-actions-and-mutations ซึ่งพูดถึง unstable_rethrow ด้วย) — นี่คือ bug แบบเดียวกันฝั่ง routing ใน Route Handler ที่เช็ค auth

app/api/redirect-bad/route.ts
import { redirect } from 'next/navigation'
// BUG: try/catch swallows redirect()
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
try {
const authed = searchParams.get('authed') === 'true'
if (!authed) redirect('/login')
return Response.json({ ok: true })
} catch (err) {
console.error('redirect-bad caught this instead of letting it redirect:', err)
return Response.json({ ok: false, error: 'caught, no real redirect happened' })
}
}

Build, start, และ request จริง (?authed=false ควรให้ redirect ทำงาน)

GET /api/redirect-bad?authed=false -> HTTP 200
{"ok":false,"error":"caught, no real redirect happened"}
server log:
redirect-bad caught this instead of letting it redirect: Error: NEXT_REDIRECT
digest: 'NEXT_REDIRECT;replace;/login;307;'

catch block ทำงาน log error ที่ redirect() throw ออกมาเป๊ะ ๆ แล้ว return JSON 200 ธรรมดาแทนที่จะ redirect เลย digest NEXT_REDIRECT;replace;/login;307; แสดงว่า Next.js คำนวณปลายทาง, ชนิด navigation (replace ค่า default นอก Server Action) และ status 307 ไว้เรียบร้อยแล้ว แต่ไม่มีอันไหนหลุดออกจาก catch เลย วิธีแก้ง่าย ๆ คือเก็บ redirect() ไว้นอก try/catch ที่ไม่ได้ rethrow มันตรง ๆ

app/api/redirect-good/route.ts
import { redirect } from 'next/navigation'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const authed = searchParams.get('authed') === 'true'
if (!authed) redirect('/login')
return Response.json({ ok: true })
}
GET /api/redirect-good?authed=false -> HTTP 307

redirect() เรียกตอน render ใน Client Component ได้ด้วย (แต่เรียกใน event handler ไม่ได้ — ใช้ useRouter แทน) และมันรับ absolute URL ได้ด้วยสำหรับ redirect ไปเว็บภายนอก

graph TD
  N["Need to navigate"] --> UI["From a link or UI?"]
  N --> LOGIC["From logic in the client?"]
  N --> SERVER["From the server, or during render?"]
  UI --> L["Link (prefetch + client nav)"]
  LOGIC --> R["useRouter push/replace/back/refresh"]
  SERVER --> S["redirect() / notFound() (throws NEXT_REDIRECT / NEXT_HTTP_ERROR_FALLBACK)"]
  S -->|"wrapped in try/catch?"| SWALLOW["Silently swallowed unless rethrown"]
เลือกเครื่องมือ navigation ตัวไหนดี

redirect() กับ permanentRedirect() ไม่ใช่ฟังก์ชันที่ “return response” — มันคือ exception ที่มี string digest ออกแบบมาเฉพาะ ที่กลไกของ Next.js เองรู้จักตอนไล่ unwind stack ตรงไหนที่เรากังวลว่า unhandled exception จะทำให้ request crash ตรงนั้นควรกังวลพอ ๆ กันว่า try/catch จะดัก error พวกนี้ไว้ก่อนที่มันจะไปถึง Next.js

เครื่องมือรันที่ไหนผลลัพธ์
<Link prefetch="auto"|null> (default)Clientprefetch route static เต็ม ๆ ส่วน dynamic แค่ถึง loading.js
<Link prefetch={true}>Clientprefetch route เต็ม ๆ เสมอ
<Link prefetch={false}>Clientไม่ prefetch เลย — ทั้ง viewport และ hover
useRouter().push/replaceClient ข้างใน event handlerclient-side navigation, เพิ่ม/แทนที่ history entry
redirect() / permanentRedirect()Server หรือตอน render ของ Client Componentthrow NEXT_REDIRECT; ถูกกลืนถ้ามี try/catch ที่ไม่ป้องกัน
  • “try/catch รอบโค้ดที่อาจ redirect ปลอดภัยกว่าเสมอ” พิสูจน์แล้วด้านบน: มันกลืน redirect 307/308 จริงไปเงียบ ๆ แล้ว return 200 แทน — เหมือนกับกรณี notFound() ที่ module มุตเมชันพิสูจน์ไว้เป๊ะ ๆ
  • “prefetch={false} ยัง prefetch ตอน hover เหมือน docs เก่า” ใน App Router ณ 16.3.6 false แปลว่าไม่เลย — ไม่ทั้ง viewport ไม่ทั้ง hover
  • “redirect() ทำงานเหมือนกันไม่ว่าจะเรียกใน event handler ของ Client Component หรือตอน render” ไม่จริง — ใน event handler ต้องใช้ useRouter().push/replace แทน

เมื่อโค้ดที่ AI สร้างทำ navigation ให้เช็คสิ่งเหล่านี้ก่อนเชื่อ

  • redirect() หรือ permanentRedirect() ถูกเรียกอยู่ใน try/catch ที่ไม่ได้ rethrow ไหม นั่นกลืน navigation ไปเลย — พิสูจน์แล้วด้านบนด้วย response 200 จริงแทนที่จะเป็น 307
  • redirect() ถูกเรียกจากข้างใน event handler ของ Client Component (onClick ไม่ใช่ตอน render) ไหม ตรงนั้นมันใช้ไม่ได้ — ต้องใช้ useRouter().push/replace แทน
  • internal navigation ใช้ <a href="/..."> ธรรมดาแทน <Link> ไหม มันยังทำงานได้ แต่เสีย client-side transition กับ prefetch ไปฟรี ๆ โดยไม่มีเหตุผล
  • prefetch={false} ถูกใช้ตรงที่จริง ๆ ต้องการ hover-prefetch ไหม ใน App Router ค่านี้ปิด prefetch ทั้งหมดเลย ไม่ใช่แค่ปิดตอน viewport
  • โค้ดสมมติว่า router.refresh() invalidate ข้อมูลที่ cache ไว้ฝั่ง server ไหม มัน clear แค่ Client Cache ของ route ปัจจุบันเท่านั้น — ใช้ revalidatePath/revalidateTag สำหรับฝั่ง server

🐛 หา bug ในโค้ดนี้

app/api/checkout/route.ts
import { redirect } from 'next/navigation'
export async function POST(request: Request) {
try {
const order = await createOrder(request)
if (order.requiresPayment) {
redirect('/pay/' + order.id)
}
return Response.json({ ok: true, order })
} catch (err) {
console.error('checkout failed', err)
return Response.json({ ok: false, error: 'checkout failed' }, { status: 500 })
}
}
เฉลย

redirect() throw error ที่มี digest แบบ NEXT_REDIRECT เพื่อให้ Next.js แปลงเป็น HTTP redirect จริงต่อไปบน stack ที่นี่มันถูกเรียกอยู่ใน try block ที่ catch ของมัน log error อะไรก็ตาม แล้ว return JSON 500 — รวมถึง error ที่ redirect() เพิ่ง throw ด้วย พิสูจน์แล้วด้วย pattern เดียวกัน build และ request จริง: response กลับมาเป็น 200/500 พร้อม JSON body แทนที่จะเป็น 307 redirect ไป /pay/[id] วิธีแก้คือเรียก redirect() หลัง try/catch (ทำ write ข้างใน block ที่ป้องกันไว้ ตัดสินใจและ redirect นอกมัน) หรือ rethrow error รูปแบบ NEXT_REDIRECT ก่อนที่ error handling ของเราเองจะทำงาน

ณ Next.js 16.3.6 prefetch={false} บน <Link> ใน App Router ทำอะไร
method ไหน refetch route ปัจจุบันโดยยังเก็บ client state แต่ไม่ invalidate server-side cache
request จริงด้วย authed=false server จะ return อะไร
try {
  if (!authed) redirect('/login')
  return Response.json({ ok: true })
} catch (err) {
  console.error(err)
  return Response.json({ ok: false })
}
navigate ออกจาก Server Component อย่างไร
เรียก redirect() ใน onClick handler ของ Client Component ได้ไหม
digest นี้บอกอะไรเราบ้าง
// server log after a real request:
Error: NEXT_REDIRECT
  digest: 'NEXT_REDIRECT;replace;/login;307;'