เขียนเว็บด้วย Next.js + TypeScript ตอนที่ 2 - ว่าด้วยเรื่อง Routing และ Dynamic Routes

Next.js Mar 30, 2023

สวัสดีครับ มาต่อกันที่ ตอนที่ 2 วันนี้จะเป็นเรื่องของ Routing และ Dynamic Routes ครับ วันนี้จะเป็นเนื้อหาเพิ่มเติมของตอนที่ 1 เกี่ยวกับการสร้างหน้าเพจ และ Route

เขียนเว็บด้วย Next.js + TypeScript ตอนที่ 1
สวัสดีครับ บทความนี้เป็นบทความชุดเขียนเว็บด้วย Next.js + TypeScript นะครับ เป็นเวอร์ชั่นที่ปรับปรุงจากบทความก่อนที่ผมเขียนไว้ Devahoy - Next.js คืออะไร? + มาหัดเขียนเว็บด้วย Next.js กันดีกว่าNext.js คืออะไร? + มาหัดเขียนเว็บด้

อย่างที่ยกตัวอย่างคร่าวๆ ในตอนที่ 1 ไปเรื่องของการสร้าง Pages ใน Next.js

  • เมื่อเราเพิ่มไฟล์ในโฟลเดอร์ pages มันก็จะเป็น routes ให้อัตโนมัติ
  • ก่อนหน้านี้เราสร้างหน้า About ก็คือเพิ่มไฟล์ pages/about.tsx จากนั้น routes ก็จะถูก generate เป็น /about ให้

Index Routes

เราสามารถกำหนดชื่อไฟล์เป็น index.tsx ก็ได้ หรือมีโฟลเดอร์หลายชั้นก็ได้ ตัว routes จะถูกแปลงเป็นแบบตัวอย่างนี้

  • pages/index.tsx/
  • pages/blog/index.tsx/blog
  • pages/about/index.tsx  /about

Nested Routes

จะทำ routing หลายๆชั้นก็ทำได้ เช่น

  • pages/blog/first-post.tsx/blog/first-post
  • pages/dashboard/settings/username.tsx/dashboard/settings/username

Dynamic Routes

รองรับการทำ Dynamic Routes สมมติเราทำเว็บมีสินค้าในระบบ 1000 ชิ้น ต้องมาเขียนไฟล์ 1000 ไฟล์ ไม่ไหวแน่ๆ ตัว Next ก็รองรับ dynamic route แบบนี้

  • ตั้งชื่อไฟล์หรือโฟลเดอร์ด้วย [] เช่น [id].tsx หรือ โฟลเดอร์ [users]
  • pages/products/[id].tsx/products/:id - เช่น /products/1 , /products/5
  • pages/post/[...all].tsx/post/* (/post/2020/id/title)

ข้อดีคือ เราใช้แค่ไฟล์เดียว ที่ match กับ :id ที่เราต้องการ

เรื่องของ Dynamic Routes

เราจะมาลงลึกในส่วน Dynamic Routes กันครับ เริ่มจาก เราจะกำหนดให้เว็บเรามี product เป็น url แบบนี้

  • /products/:id เช่น /products/1 , /products/2

สิ่งที่เราต้องทำ ก็คือ สร้างไฟล์ [id].tsx ขึ้นมา ในโฟลเดอร์ pages/products

const ProductDetail = () => {
  return (
    <h1>This is Product Detail Page</h1>
  )
}

export default ProductDetail

เราสามารถ get params จาก [id] ได้ด้วยการใช้ next/router

import { useRouter } from 'next/router'

const ProductDetail = () => {
const router = useRouter()
const { id } = router.query

  return (
    <h1>This is Product {id}</h1>
  )
}

export default ProductDetail

จากโค๊ดด้านบน ตัว Router จะ get object จาก params ที่เราตั้งชื่อไว้แบบเดียวกับชื่อไฟล์ นอกจากนี้ ตัว Router ยัง get query parameters ด้วยนะครับ ตัวอย่าง pages แบบต่างๆ จะได้ค่า query ยังไงบ้าง?

  • pages/products/[id]/[comment].tsx -> pages/products/1/my-comment
{ "id": "1", "comment": "my-comment" }
  • /pages/products/100?sku=test
{ "id": "100", "sku": "test }

ทีนี้เราลองทดสอบด้วยการ start dev server ขึ้นมา แล้วลองเข้าเว็บ ด้วย url ต่างๆ จะเห็นค่า product id ที่แตกต่างกัน

npm run dev
  • http://localhost:3000/products/1
  • http://localhost:3000/products/50

Catch all routes

ตัว Dynamic routes เรายังสามารถ ดักจับ path ทั้งหมด ได้ด้วยการใช้ 3จุด (...) ข้างใน [] ตัวอย่างเช่น

  • pages/posts/[...all].js จะ match ทั้ง /posts/1, /posts/1/2/3, /posts/1/comments อะไรก็แล้วแต่ ที่ตามหลัง /posts จะเข้าเงื่อนไขทั้งหมด
// เช่น path /posts/1/comments/1000

const { all } = router.query
// all: ['1', 'comments', '1000']

ในตอนที่ 1 พูดไปแล้ว เรื่องการใช้ next/link ตัวอย่างก่อนหน้านี้

<Link href="/products/1">Go to Product 1</Link>

เราอยากใส่ Link อะไรก็แค่เปลี่ยน href ใช่มั้ยครับ หรือถ้าจะส่ง query string ไปด้วย ก็เป็น href="/products/1?color=red&size=m ก็ทำได้

หรือ เราจะส่งแบบ URL Object แบบนี้ก็ได้ ไม่ต้องนำ query มาต่อ url ให้วุ่นวาย

<Link
  href={{
    pathname: '/products/[id]',
    query: { id: 1 },
  }}
>
 Product #1
</Link>

<Link
  href={{
    pathname: '/products/[id]',
    query: { id: 1, color: "red", size: "m" },
  }}
>
 Product #1
</Link>

ลงมือโค๊ดดีกว่า

ลงมือทำดีกว่า เพื่อให้เห็นภาพจริงๆ  เริ่มจาก ลองทำหน้าขึ้นมา หน้า 1 เป็นหน้า Product List ครับ ผมตั้งชื่อว่า pages/products/index.tsx มีข้อมูล mockProducts ใส่ไปด้วย เป็น ดังนี้

import Link from 'next/link'

const mockProducts = [
  {
    id: 1,
    title: 'Super cool T-Shirt',
    size: 'm',
    color: 'red',
  },
  {
    id: 2,
    title: 'Hello T-Shirt',
    size: 'l',
    color: 'black',
  },
  {
    id: 3,
    title: 'Ahoy Hoodie',
    size: 'l',
    color: 'black',
  },
]

interface Product {
  id: number
  title: string
  size: string
  color: string
}

export default function Products() {
  return (
    <div>
      <h2>My Products</h2>

      <ul>
        {mockProducts.map(({ id, title, size, color }: Product) => {
          return (
            <li key={id}>
              <Link
                href={{
                  pathname: '/products/[id]',
                  query: { id, size, color },
                }}
              >
                {title}
              </Link>
            </li>
          )
        })}
      </ul>
    </div>
  )
}

จากนั้นสร้างอีกไฟล์ สำหรับ Product Detail ชื่อ pages/products/[id].tsx ตอนนี้เราจะมี 2 ไฟล์ ใน​โฟลเดอร์ pages/products แล้วนะครับ คือ

  • index.tsx - หน้า Products รวม จะเป็น route  → /products
  • [id].tsx - หน้า Detail จะเป็น route  → /products/:id

ไฟล์ products/[id].tsx จะมีข้อมูลดังนี้ แค่แสดงข้อมูล id จาก router

import { useRouter } from 'next/router'
import Link from 'next/link'

const ProductDetail = () => {
  const router = useRouter()
  const { id, color, size } = router.query

  return (
    <div>
      <h1>This is Product {id}</h1>
      <p>Color : {color}</p>
      <p>Size : {size}</p>
      <Link href="/products">List Products</Link>
    </div>
  )
}

export default ProductDetail

ทดลอง start dev server ดูผลลัพธ์

npm run dev

🎉 Done!

จบไปแล้วครับสำหรับตอนที่ 2 ไม่ได้เน้นหน้าตา UI นะครับ เน้นให้เข้าใจการทำงานของ Routes และ Dynamic Routes รวมถึงการอ่านค่าจาก router params เป็นต้น

Tags

Chai Phonbopit

เป็น Web Dev ทำงานมา 10 ปีหน่อยๆ ด้วยภาษา JavaScript, Node.js, React, Vue และปัจจุบันกำลังสนใจ Web3, Crypto และ Blockchain เขียนบล็อกที่ https://devahoy.com