Back

React & Next.js: Components, State, Hooks และ RoutingBlur image

บทความนี้แนะนำการสร้างเว็บแอปพลิเคชันด้วย React และ Next.js ตั้งแต่พื้นฐาน โดยสร้างโปรเจกต์ใหม่ชื่อ react-nextjs-101 และเรียนรู้แนวคิดหลักที่ใช้ทำงานจริง ได้แก่ การแยก component, การส่งข้อมูลด้วย props, การจัดการ state ด้วย hooks, การดึงข้อมูลจาก API และการทำ routing

เนื้อหาแบ่งเป็น 3 ส่วนที่ต่อยอดกัน:

  1. ส่วนที่ 1 — Components และ Props: แยก component เป็นไฟล์ ส่งข้อมูลด้วย props และแสดงรายการด้วย map()
  2. ส่วนที่ 2 — State และ Hooks: useState, การจัดการ event, useEffect และการดึงข้อมูลจาก API
  3. ส่วนที่ 3 — Routing ใน Next.js: file-based routing, dynamic routes, Link และ useRouter

ข้อกำหนดเบื้องต้น: ควรมีพื้นฐาน HTML, CSS, JavaScript และติดตั้ง Node.js กับ VS Code ไว้แล้ว (ดูสรุปในหัวข้อถัดไป) ตัวอย่าง API ในบทความนี้ใช้จาก Me Live Code API ↗


การเตรียมเครื่องมือ#

ก่อนเริ่มต้องมีเครื่องมือสองอย่างนี้ในเครื่อง

  • VS Code — โปรแกรมสำหรับเขียนโค้ด ติดตั้งจาก code.visualstudio.com ↗
  • Node.js (LTS) — สภาพแวดล้อมสำหรับรัน JavaScript นอกเบราว์เซอร์ ติดตั้งจาก nodejs.org ↗

จากนั้นสร้างโปรเจกต์ Next.js ใหม่ชื่อ react-nextjs-101 (--js บังคับใช้ JavaScript แทน TypeScript):

# สร้างโปรเจกต์ Next.js ใหม่
npx create-next-app@16 react-nextjs-101 --js

# เข้าไปในโปรเจกต์ แล้วเปิด VS Code
cd react-nextjs-101
code .
bash

เมื่อเปิด VS Code ด้วย code . แล้ว การเริ่ม development server ครั้งแรกทำตามขั้นตอนนี้

1. เปิด Terminal ใน VS Code — จากเมนู Terminal → New Terminal (หรือกดคีย์ลัด Ctrl+Shift+`)

เปิด New Terminal จากเมนู Terminal ใน VS Code

2. เลือก Command Prompt (สำหรับ Windows) — หาก Terminal เปิดขึ้นมาเป็น PowerShell ให้กดลูกศร ∨ ข้างปุ่ม + มุมขวาบนของ Terminal แล้วเลือก Command Prompt เพื่อให้คำสั่งทำงานตรงกับตัวอย่างในบทความ

เลือก Command Prompt จากรายการ profile ของ Terminal

3. รัน npm run dev — พิมพ์คำสั่งต่อไปนี้แล้วกด Enter

npm run dev
bash

เมื่อ server พร้อมใช้งานจะแสดงลิงก์ Local: http://localhost:3000 กด Ctrl ค้างแล้วคลิกที่ลิงก์ VS Code จะเปิดเบราว์เซอร์ในตัว (Simple Browser) แสดงหน้าแอปขึ้นมาทันทีโดยไม่ต้องออกจาก VS Code

ผลลัพธ์หลังรัน npm run dev แสดงลิงก์ localhost:3000 และหน้าแอปในเบราว์เซอร์ของ VS Code


การใส่ CSS ในโปรเจกต์#

ในเว็บทั่วไปใส่สไตล์ผ่านไฟล์ .css ที่ลิงก์ไว้ใน HTML ส่วน Next.js ใช้ global stylesheet โดยสร้างไฟล์ CSS แล้ว import เข้าไปใน app/layout.js เพียงครั้งเดียว สไตล์จะมีผลกับทุกหน้า

สร้างไฟล์ app/style.css:

body {
  font-family: sans-serif;
  margin: 0;
  padding: 20px;
  background-color: #f5f5f5;
}

h1 {
  color: #333;
}
css

ไฟล์ app/layout.js คือ root layout ที่ครอบทุกหน้าในแอป โดยกำหนดโครง <html> และ <body> ร่วมกัน เนื้อหาของแต่ละหน้าจะถูกส่งเข้ามาแทนที่ {children} การ import CSS ไว้ในไฟล์นี้ครั้งเดียวจึงมีผลกับทุกหน้า นอกจากนี้ metadata ยังใช้ตั้งค่า title (ข้อความบนแท็บเบราว์เซอร์) และ description สำหรับ SEO

แก้ app/layout.js โดยลบบรรทัด import "./globals.css" ที่ create-next-app สร้างไว้ ใช้ import "./style.css" แทน และเปลี่ยน title:

import { Geist, Geist_Mono } from "next/font/google";
import "./style.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata = {
  title: "React & Next.js basics",
  description: "React & Next.js practice project",
};

export default function RootLayout({ children }) {
  return (
    <html
      lang="en"
      className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
    >
      <body className="min-h-full flex flex-col">{children}</body>
    </html>
  );
}
javascript

จากนั้นแก้ app/page.js (หน้าแรกที่ create-next-app สร้างไว้) ให้เหลือแค่ hello world เพื่อดูผลหลังใส่ CSS — เปิดที่ http://localhost:3000 ↗

export default function Home() {
  return <h1>Hello World</h1>;
}
javascript

Hello World หลังใส่ CSS

หมายเหตุ: ตัวอย่างในบทความนี้ยังใช้ inline style (style={{ ... }}) เพื่อความกระชับ แต่ในงานจริงควรแยกสไตล์ไว้ในไฟล์ .css เพื่อให้ดูแลรักษาง่ายกว่า


ส่วนที่ 1: Components และ Props#

1.1 React Component#

React เขียน component เป็นฟังก์ชันที่ return ค่า JSX กลับมา จุดแข็งของ React คือสามารถแยก component ออกเป็นชิ้นเล็กๆ ที่นำกลับมาใช้ซ้ำได้ แทนที่จะเขียนโค้ดยาวๆ ไว้ในไฟล์เดียว

โดยทั่วไปจะเก็บ component ที่ใช้ซ้ำไว้ในโฟลเดอร์ app/components/

1.2 การแยก Component เป็นไฟล์#

สร้างไฟล์ app/components/Greeting.js:

export default function Greeting() {
  return <h2>Hello from a component!</h2>;
}
javascript

จากนั้นสร้างไฟล์ app/01_greeting/page.js — เข้าถึงที่ http://localhost:3000/01_greeting ↗ แล้ว import Greeting มาใช้ (ไฟล์อยู่ลึกลงไปหนึ่งชั้น จึง import ด้วย ../components/):

import Greeting from "../components/Greeting";

export default function GreetingPage() {
  return (
    <div>
      <h1>My App</h1>
      <Greeting />
      <Greeting />
    </div>
  );
}
javascript

Greeting ถูกเรียกใช้ได้หลายครั้งเหมือนแท็ก HTML ทั่วไป นี่คือหัวใจของการนำ component กลับมาใช้ซ้ำ

เปิด http://localhost:3000/01_greeting ↗ จะเห็น Greeting ตัวเดียวกันแสดงผลซ้ำสองครั้ง:

หน้า /01_greeting แสดง Greeting เดียวกันสองครั้ง

1.3 Props — การส่งข้อมูลเข้า Component#

Props (properties) คือช่องทางส่งข้อมูลจาก component แม่ไปยัง component ลูก ทำให้ component เดิมแสดงผลต่างกันได้ตามค่าที่รับเข้ามา

Props data flow diagram

แก้ไขไฟล์ app/components/Greeting.js ให้รับ prop ชื่อ name:

export default function Greeting({ name }) {
  return <h2>Hello, {name}!</h2>;
}
javascript

ส่งค่า name ตอนเรียกใช้ใน app/01_greeting/page.js:

<Greeting name="Karn" />
<Greeting name="Somchai" />
javascript

เปิด http://localhost:3000/01_greeting ↗ จะเห็นผลลัพธ์แบบนี้:

หน้า /01_greeting แสดง Greeting ที่รับ prop name

ตัวอย่าง: Component ที่รับหลาย props

สร้างไฟล์ app/components/Card.js:

export default function Card({ title, description }) {
  return (
    <div style={{ border: "1px solid #ccc", borderRadius: "8px", padding: "16px", margin: "10px" }}>
      <h3>{title}</h3>
      <p>{description}</p>
    </div>
  );
}
javascript
แนวคิดคำอธิบาย
propsออบเจกต์ที่รวมข้อมูลทั้งหมดที่ส่งเข้า component
{ title, description }destructuring — ดึงค่าออกจาก props โดยตรง
name="Karn"ส่ง string เป็น prop
count={5}ส่งค่าที่ไม่ใช่ string ต้องครอบด้วย { }

หมายเหตุ: props เป็นแบบ อ่านอย่างเดียว (read-only) — component ลูกห้ามแก้ค่า props ที่รับมา ถ้าต้องการค่าที่เปลี่ยนแปลงได้ให้ใช้ state (ดูส่วนที่ 2)

1.4 การแสดงรายการด้วย Props และ map()#

รูปแบบที่พบบ่อยที่สุดคือการรวม map() เข้ากับ component เพื่อแสดงรายการข้อมูลเป็นการ์ดหลายใบ

List rendering with map() diagram

สร้างไฟล์ app/02_card/page.js — เข้าถึงที่ http://localhost:3000/02_card ↗:

import Card from "../components/Card";

export default function CardPage() {
  const courses = [
    { id: 1, title: "HTML & CSS", description: "Structure and style web pages" },
    { id: 2, title: "JavaScript", description: "Add logic and interactivity" },
    { id: 3, title: "React", description: "Build reusable UI components" }
  ];

  return (
    <div style={{ padding: "20px" }}>
      <h1>Courses</h1>
      {courses.map((course) => (
        <Card key={course.id} title={course.title} description={course.description} />
      ))}
    </div>
  );
}
javascript

แต่ละรอบของ map() จะสร้าง <Card> หนึ่งใบพร้อมส่งข้อมูลผ่าน props อย่าลืมใส่ prop key ที่ไม่ซ้ำกันให้ทุกรายการ

เปิด http://localhost:3000/02_card ↗ จะเห็นผลลัพธ์แบบนี้:

หน้า /02_card แสดงรายการ Courses ด้วย Card

1.5 children Prop#

นอกจากส่งข้อมูลผ่าน props แบบมีชื่อแล้ว ยังส่ง เนื้อหา JSX เข้าไปในตัว component ได้ผ่าน prop พิเศษชื่อ children

สร้างไฟล์ app/components/Panel.js:

export default function Panel({ title, children }) {
  return (
    <div style={{ border: "1px solid #ccc", borderRadius: "8px", padding: "16px", margin: "10px" }}>
      <h3>{title}</h3>
      <div>{children}</div>
    </div>
  );
}
javascript

ทุกอย่างที่อยู่ระหว่างแท็กเปิดและปิดของ <Panel> จะกลายเป็น children:

<Panel title="Announcement">
  <p>The store opens at 9:00 AM.</p>
  <p>Free shipping on orders over $50.</p>
</Panel>
javascript

นำ Panel ไปใช้จริงโดยแก้ไฟล์ app/02_card/page.js จากหัวข้อ 1.4 — เพิ่ม import Panel แล้ววาง <Panel> ไว้เหนือรายการ Courses:

import Card from "../components/Card";
import Panel from "../components/Panel";

export default function CardPage() {
  const courses = [
    { id: 1, title: "HTML & CSS", description: "Structure and style web pages" },
    { id: 2, title: "JavaScript", description: "Add logic and interactivity" },
    { id: 3, title: "React", description: "Build reusable UI components" }
  ];

  return (
    <div style={{ padding: "20px" }}>
      <Panel title="Announcement">
        <p>The store opens at 9:00 AM.</p>
        <p>Free shipping on orders over $50.</p>
      </Panel>
      <h1>Courses</h1>
      {courses.map((course) => (
        <Card key={course.id} title={course.title} description={course.description} />
      ))}
    </div>
  );
}
javascript

Panel รับเนื้อหาผ่าน children ส่วน Card รับข้อมูลผ่าน props แบบมีชื่อ — component สองรูปแบบนี้ทำงานร่วมกันในหน้าเดียวได้


ส่วนที่ 2: State และ Hooks#

จนถึงตอนนี้หน้าเว็บยังแสดงข้อมูลแบบคงที่ ส่วนนี้จะทำให้หน้าเว็บโต้ตอบได้ โดยใช้ Hooks ซึ่งเป็นฟังก์ชันพิเศษของ React ที่ขึ้นต้นด้วยคำว่า use

2.1 Server Component และ Client Component#

ใน Next.js (App Router) component ทุกตัวเป็น Server Component โดยค่าเริ่มต้น ซึ่งเรนเดอร์บนเซิร์ฟเวอร์และใช้ hooks หรือจัดการ event ไม่ได้

หากต้องการใช้ state, event หรือ hooks ต้องประกาศให้ component นั้นเป็น Client Component โดยใส่ "use client" ไว้บรรทัดบนสุดของไฟล์

"use client";
javascript

หมายเหตุ: ใส่ "use client" เฉพาะไฟล์ที่ต้องการโต้ตอบกับผู้ใช้ (มี state/event) เท่านั้น component ที่แสดงข้อมูลอย่างเดียวปล่อยให้เป็น Server Component จะดีต่อประสิทธิภาพมากกว่า

2.2 useState — การเก็บค่าที่เปลี่ยนแปลงได้#

useState ใช้เก็บค่าที่เปลี่ยนแปลงได้ในระหว่างที่ผู้ใช้โต้ตอบกับหน้าเว็บ เมื่อค่าเปลี่ยน React จะเรนเดอร์ component ใหม่ให้อัตโนมัติ

useState re-render loop diagram

ตัวอย่างที่ใช้ได้จริงคือ ช่องเลือกจำนวนสินค้า ในหน้าสั่งซื้อ — เมื่อผู้ใช้กดเพิ่ม/ลดจำนวน state จะเก็บจำนวนล่าสุดไว้ และคำนวณราคารวมใหม่ให้ทันที

สร้างไฟล์ app/03_product/page.js — เข้าถึงที่ http://localhost:3000/03_product ↗

"use client";

import { useState } from "react";

export default function ProductPage() {
  const [quantity, setQuantity] = useState(1);
  const price = 120;

  return (
    <div>
      <h1>Green Tea</h1>
      <p>Price: ฿{price}</p>
      <button onClick={() => setQuantity(quantity - 1)} disabled={quantity <= 1}>-</button>
      <span> {quantity} </span>
      <button onClick={() => setQuantity(quantity + 1)}>+</button>
      <p>Total: ฿{price * quantity}</p>
    </div>
  );
}
javascript

เปิด http://localhost:3000/03_product ↗ แล้วกดปุ่ม + / − จะเห็นจำนวนและราคารวมเปลี่ยนทันที:

หน้า /03_product แสดงช่องเลือกจำนวนสินค้า Green Tea

ส่วนคำอธิบาย
useState(1)สร้าง state เก็บจำนวนสินค้า เริ่มต้นที่ 1
quantityค่าปัจจุบันของ state (จำนวนที่เลือก)
setQuantityฟังก์ชันสำหรับอัปเดตจำนวน
price * quantityราคารวมที่คำนวณใหม่ทุกครั้งที่ state เปลี่ยน
disabled={quantity <= 1}ปิดปุ่มลดเมื่อจำนวนเหลือ 1 กันไม่ให้ติดลบ

หมายเหตุ: ห้ามแก้ค่า state โดยตรง (เช่น quantity = quantity + 1) ต้องเรียกผ่านฟังก์ชัน setter (setQuantity) เสมอ React จึงจะรู้ว่าต้องเรนเดอร์ใหม่

2.3 การจัดการ Event และ Controlled Input#

เมื่อผู้ใช้พิมพ์ในช่อง input สามารถเก็บค่านั้นไว้ใน state ได้ทันที รูปแบบนี้เรียกว่า controlled component คือให้ค่าใน input ถูกควบคุมด้วย state ของ React ตัวอย่างที่ใช้ได้จริงคือ ฟอร์มล็อกอิน — เก็บค่า username/password ไว้ใน state แล้วตรวจสอบตอนกดปุ่ม submit

สร้างไฟล์ app/04_form/page.js — เข้าถึงที่ http://localhost:3000/04_form ↗

"use client";

import { useState } from "react";

export default function LoginPage() {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [message, setMessage] = useState("");

  const handleSubmit = (event) => {
    event.preventDefault(); // stop the browser from reloading the page
    if (username === "admin" && password === "admin") {
      setMessage("Login success");
    } else {
      setMessage("Invalid username or password");
    }
  };

  return (
    <div>
      <h1>Login</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={username}
          onChange={(event) => setUsername(event.target.value)}
          placeholder="Username"
        />
        <input
          type="password"
          value={password}
          onChange={(event) => setPassword(event.target.value)}
          placeholder="Password"
        />
        <button type="submit">Login</button>
      </form>
      {/* show the result after the form is submitted */}
      {message && <p>{message}</p>}
    </div>
  );
}
javascript

เปิด http://localhost:3000/04_form ↗ กรอก admin / admin แล้วกด Login จะเห็นข้อความ Login success:

หน้า /04_form ฟอร์มล็อกอินที่แสดง Login success

ส่วนคำอธิบาย
value={username} / value={password}ผูกค่าของแต่ละ input กับ state
onChangeอัปเดต state ทุกครั้งที่ค่าใน input เปลี่ยน
onSubmitevent ที่ทำงานเมื่อกดปุ่ม Login หรือกด Enter
event.preventDefault()กันไม่ให้เบราว์เซอร์รีโหลดหน้าตอน submit ฟอร์ม
username === "admin" && password === "admin"เงื่อนไขตรวจสอบล็อกอิน

หมายเหตุ: การตรวจสอบ admin/admin ในตัวอย่างนี้ทำฝั่ง client เพื่อสาธิตเท่านั้น ระบบจริงต้องยืนยันตัวตนที่เซิร์ฟเวอร์

2.4 useEffect — การทำงานเมื่อ Component เปลี่ยนแปลง#

useEffect ใช้สั่งให้โค้ดทำงานหลังจาก component เรนเดอร์ เช่น การตั้งเวลา การดึงข้อมูล หรือการทำงานกับสิ่งภายนอก React

useEffect localStorage diagram

ตัวอย่างที่ใช้ได้จริงคือ ช่องกรอกบันทึกที่เซฟอัตโนมัติ — โหลดข้อความเดิมจาก localStorage ตอนเปิดหน้า และเซฟกลับทุกครั้งที่ผู้ใช้พิมพ์ ทำให้ข้อความไม่หายแม้รีเฟรชหน้า

สร้างไฟล์ app/05_note/page.js — เข้าถึงที่ http://localhost:3000/05_note ↗

"use client";

import { useState, useEffect } from "react";

export default function NotePage() {
  const [note, setNote] = useState("");

  // load the saved note once when the page opens
  useEffect(() => {
    const saved = localStorage.getItem("note");
    if (saved) setNote(saved);
  }, []);

  // save the note whenever it changes
  useEffect(() => {
    localStorage.setItem("note", note);
  }, [note]);

  return (
    <div>
      <h1>Auto-saved Note</h1>
      <textarea
        value={note}
        onChange={(event) => setNote(event.target.value)}
        placeholder="Type a note — it is saved automatically"
      />
      <p>Refresh the page — the note stays.</p>
    </div>
  );
}
javascript

เปิด http://localhost:3000/05_note ↗ พิมพ์ข้อความแล้วรีเฟรชหน้า ข้อความจะยังอยู่:

หน้า /05_note ช่องบันทึกที่เซฟอัตโนมัติ

Dependency array (อาร์เรย์ตัวที่สองของ useEffect) เป็นตัวกำหนดว่า effect จะทำงานเมื่อใด

Dependency arrayeffect ทำงานเมื่อ
[] (ว่าง)ทำงานครั้งเดียวตอน component โหลดเสร็จ
[value]ทำงานทุกครั้งที่ value เปลี่ยน
ไม่ใส่เลยทำงานทุกครั้งที่เรนเดอร์ (ควรหลีกเลี่ยง)

2.5 การดึงข้อมูลจาก API#

เมื่อรวม useState กับ useEffect เข้าด้วยกัน สามารถดึงข้อมูลจาก API มาแสดงบนหน้าเว็บได้ ตัวอย่างนี้ใช้ Attractions API จาก Me Live Code API ↗ ที่คืนข้อมูลสถานที่ท่องเที่ยวเป็นอาร์เรย์

JSON (JavaScript Object Notation) คือรูปแบบข้อความมาตรฐานสำหรับแลกเปลี่ยนข้อมูลระหว่างเซิร์ฟเวอร์กับแอป โครงสร้างหลักมีสองแบบ — อ็อบเจกต์ ครอบด้วย { } เก็บข้อมูลเป็นคู่ "key": value และ อาร์เรย์ ครอบด้วย [ ] เก็บรายการหลายค่าเรียงต่อกัน โดยค่า (value) เป็นได้ทั้งข้อความ ("..."), ตัวเลข, true / false, null, หรืออ็อบเจกต์และอาร์เรย์ซ้อนกัน

ก่อนเขียนโค้ด ลองเปิด https://melivecode.com/api/attractions ↗ บนเบราว์เซอร์เพื่อดูข้อมูลที่ API ส่งกลับมา — เป็น อาร์เรย์ของอ็อบเจกต์ สถานที่ท่องเที่ยว (ตัดมาแสดง 2 รายการจากทั้งหมด 12 รายการ):

[
  {
    "id": 1,
    "name": "Phi Phi Islands",
    "detail": "Phi Phi Islands are a group of islands in Thailand between the large island of Phuket and the Malacca Coastal Strait of Thailand.",
    "coverimage": "https://www.melivecode.com/attractions/1.jpg",
    "latitude": 7.737619,
    "longitude": 98.7068755,
    "createdAt": "2026-09-18T07:46:52.999Z"
  },
  {
    "id": 2,
    "name": "Eiffel Tower",
    "detail": "Eiffel Tower is one of the most famous structures in the world. Eiffel Tower is named after a leading French architect and engineer. It was built as a symbol of the World Fair in 1889.",
    "coverimage": "https://www.melivecode.com/attractions/2.jpg",
    "latitude": 48.8583736,
    "longitude": 2.2922926,
    "createdAt": "2026-09-18T07:46:52.999Z"
  }
]
json

แต่ละอ็อบเจกต์มีฟิลด์ดังนี้:

ฟิลด์คำอธิบาย
idรหัสประจำสถานที่ (ใช้เป็น key และใช้เปิดหน้ารายละเอียด)
nameชื่อสถานที่
detailรายละเอียดของสถานที่
coverimageURL ของรูปภาพหน้าปก
latitude / longitudeพิกัดตำแหน่งบนแผนที่
createdAtวันเวลาที่เพิ่มข้อมูล

เนื่องจาก API คืนค่าเป็นอาร์เรย์โดยตรง จึงเก็บลง state ได้ทันทีด้วย setAttractions(data) แล้ววนด้วย map() เพื่อแสดงแต่ละรายการ

สร้างไฟล์ app/06_attractions/page.js — เข้าถึงที่ http://localhost:3000/06_attractions ↗

"use client";

import { useState, useEffect } from "react";

export default function AttractionsPage() {
  const [attractions, setAttractions] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // fetch the list of attractions when the page loads
    fetch("https://melivecode.com/api/attractions")
      .then((response) => response.json())
      .then((data) => {
        setAttractions(data);
        setLoading(false);
      });
  }, []);

  // show a loading message while the data is being fetched
  if (loading) {
    return <p>Loading...</p>;
  }

  return (
    <div style={{ padding: "20px" }}>
      <h1>Attractions</h1>
      {/* flex container: cards wrap to the next row on small screens */}
      <div className="attractions">
        {attractions.map((attraction) => (
          <div key={attraction.id} className="attraction-card">
            <h2>{attraction.name}</h2>
            <img src={attraction.coverimage} alt={attraction.name} width="200" />
          </div>
        ))}
      </div>
    </div>
  );
}
javascript

Responsive ด้วย flexbox — แทนที่จะใช้ inline style ให้แยกสไตล์ไว้ในไฟล์ CSS ที่สร้างไว้ก่อนหน้า เพิ่มโค้ดนี้ต่อท้าย app/style.css:

/* ...existing styles... */

.attractions {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}

.attraction-card {
  width: 220px;
  border: 1px solid #e0e0e0;
  border-radius: 10px;
  padding: 12px;
  background-color: #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}

.attraction-card h2 {
  font-size: 1.1rem;
  margin: 8px 0 0;
  color: #333;
  /* truncate a long name to a single line with an ellipsis */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.attraction-card img {
  width: 100%;
  height: 140px;
  object-fit: cover;
  border-radius: 6px;
}
css

display: flex กับ flex-wrap: wrap ทำให้การ์ดที่เกินตกลงไปขึ้นแถวใหม่เองเมื่อจอแคบลง ส่วน gap เว้นระยะห่างระหว่างการ์ด มุมโค้ง (border-radius) และเงา (box-shadow) ช่วยให้การ์ดดูนุ่มนวลขึ้น และ object-fit: cover ทำให้รูปทุกใบมีขนาดเท่ากันโดยไม่ผิดสัดส่วน ส่วนชื่อสถานที่ตัดด้วย text-overflow: ellipsis เมื่อยาวเกินหนึ่งบรรทัดจะแสดงเป็น ... แทน การ์ดจึงสูงเท่ากันทุกใบ

เปิด http://localhost:3000/06_attractions ↗ จะเห็นการ์ดเรียงแบบ responsive:

หน้า /06_attractions แสดงการ์ดสถานที่ท่องเที่ยวแบบ responsive

รูปแบบข้อมูลที่ API คืนกลับมา — GET /api/attractions คืนอาร์เรย์ของออบเจกต์ในรูปแบบนี้:

[
  {
    "id": 1,
    "name": "Phi Phi Islands",
    "detail": "...",
    "coverimage": "https://www.melivecode.com/attractions/1.jpg",
    "latitude": 7.7407,
    "longitude": 98.7784
  }
]
javascript
ขั้นตอนคำอธิบาย
useState([])เก็บข้อมูลที่ดึงมา เริ่มต้นเป็นอาร์เรย์ว่าง
useState(true)เก็บสถานะ loading เริ่มต้นเป็น true
fetch(url)ส่ง request ไปยัง API
.then(res => res.json())แปลงผลลัพธ์เป็น JSON
setAttractions(data)เก็บข้อมูลลง state แล้วเรนเดอร์ใหม่

หมายเหตุ: การใส่ state loading ช่วยให้ผู้ใช้เห็นข้อความระหว่างรอข้อมูล แทนที่จะเห็นหน้าว่างเปล่า


ส่วนที่ 3: Routing ใน Next.js#

Next.js ใช้ระบบ file-based routing — โครงสร้างโฟลเดอร์ภายใต้ app/ จะกลายเป็น URL ของหน้าเว็บโดยอัตโนมัติ ไม่ต้องตั้งค่า route เอง

Next.js file-based routing diagram

3.1 File-based Routing#

แต่ละโฟลเดอร์ที่มีไฟล์ page.js จะกลายเป็นหนึ่งหน้าเว็บ

ตำแหน่งไฟล์URL
app/page.js/
app/01_greeting/page.js/01_greeting
app/02_card/page.js/02_card
app/03_product/page.js/03_product
app/06_attractions/page.js/06_attractions
app/06_attractions/[id]/page.js/06_attractions/1, /06_attractions/2, …

3.2 Dynamic Routes#

หากต้องการหน้ารายละเอียดที่เปลี่ยนตาม id ให้สร้างโฟลเดอร์ชื่อครอบด้วยวงเล็บเหลี่ยม [id] ค่าใน URL จะถูกส่งเข้ามาเป็น parameter

เริ่มจากตัวอย่างง่ายที่สุดเพื่อดูว่า dynamic route ทำงานอย่างไร — สร้างไฟล์ app/06_attractions/[id]/page.js ที่นำค่า id จาก URL มาแสดงเฉย ๆ:

"use client";

import { useParams } from "next/navigation";

export default function Page() {
  const { id } = useParams();
  return (
    <div>
      <h1>id: {id}</h1>
    </div>
  );
}
javascript

เปิด http://localhost:3000/06_attractions/1 ↗ จะเห็น id: 1 และถ้าเปลี่ยนเป็น http://localhost:3000/06_attractions/2 ↗ จะเห็น id: 2 — ค่าในตำแหน่ง [id] ของ URL ถูกส่งเข้ามาผ่าน useParams() โดยอัตโนมัติ ไม่ว่าจะใส่เลขใดก็เข้าหน้าเดียวกันนี้

หน้า /06_attractions/1 แสดงค่า id จาก URL ด้วย useParams

นอกจาก endpoint ที่คืนสถานที่ทั้งหมดในหัวข้อ 2.5 แล้ว API ยังมี endpoint สำหรับดึงข้อมูล ทีละรายการ ตาม id โดยเปลี่ยนเลขท้าย URL เช่น https://melivecode.com/api/attractions/1 ↗ หรือ https://melivecode.com/api/attractions/2 ↗ ต่างจาก endpoint รายการตรงที่ไม่ได้คืนเป็นอาร์เรย์ แต่ห่อข้อมูลไว้ในอ็อบเจกต์ { status, attraction }:

{
  "status": "ok",
  "attraction": {
    "id": 1,
    "name": "Phi Phi Islands",
    "detail": "Phi Phi Islands are a group of islands in Thailand between the large island of Phuket and the Malacca Coastal Strait of Thailand.",
    "coverimage": "https://www.melivecode.com/attractions/1.jpg",
    "latitude": 7.737619,
    "longitude": 98.7068755,
    "createdAt": "2026-09-18T07:46:52.999Z"
  }
}
json
ฟิลด์คำอธิบาย
statusสถานะของ request — "ok" เมื่อสำเร็จ
attractionอ็อบเจกต์สถานที่รายตัว (ฟิลด์ภายในเหมือนกับในหัวข้อ 2.5)

ข้อมูลที่ต้องการอยู่ในฟิลด์ attraction จึงอ่านด้วย data.attraction ไม่ใช่ data ทั้งก้อน

ตอนนี้แก้ไฟล์ app/06_attractions/[id]/page.js เดิม ให้นำ id ไปดึงข้อมูลจริงจาก API มาแสดงแทนการโชว์แค่ตัวเลข:

"use client";

import { useState, useEffect } from "react";
import { useParams } from "next/navigation";
import Link from "next/link";

export default function AttractionDetailPage() {
  const { id } = useParams();
  const [attraction, setAttraction] = useState(null);

  useEffect(() => {
    // fetch a single attraction by its id from the URL
    fetch(`https://melivecode.com/api/attractions/${id}`)
      .then((response) => response.json())
      .then((data) => setAttraction(data.attraction));
  }, [id]);

  if (!attraction) {
    return <p>Loading...</p>;
  }

  return (
    <div style={{ padding: "20px" }}>
      {/* link back to the list page */}
      <Link href="/06_attractions">← Back to attractions</Link>
      <h1>{attraction.name}</h1>
      <img src={attraction.coverimage} alt={attraction.name} width="300" />
      <p>{attraction.detail}</p>
    </div>
  );
}
javascript

GET /api/attractions/{id} คืนข้อมูลรายตัวในรูปแบบ { status, attraction } จึงอ่านค่าจาก data.attraction ส่วน Link ด้านบนพากลับไปหน้ารายการ (รายละเอียดของ Link อยู่ในหัวข้อ 3.3)

เปิด http://localhost:3000/06_attractions/1 ↗ จะเห็นหน้ารายละเอียดพร้อมลิงก์กลับ:

หน้า /06_attractions/1 แสดงรายละเอียดสถานที่พร้อมลิงก์กลับ

ส่วนคำอธิบาย
[id]ชื่อโฟลเดอร์แบบ dynamic — รับค่าจาก URL
useParams()hook ที่คืนค่า parameter จาก URL
[id] ใน dependencyดึงข้อมูลใหม่เมื่อ id เปลี่ยน

React/Next.js ใช้ component Link แทนแท็ก <a> เพื่อให้เปลี่ยนหน้าได้ทันทีโดยไม่โหลดหน้าใหม่ทั้งหมด

แก้ไข app/06_attractions/page.js จากหัวข้อ 2.5 โดยครอบการ์ดแต่ละใบด้วย Link ให้ลิงก์ไปยังหน้ารายละเอียด — เพิ่ม import และแก้เฉพาะส่วน map():

"use client";

import { useState, useEffect } from "react";
import Link from "next/link";

/* ...existing useState and useEffect... */

// inside the flex container, wrap each card with a Link:
{attractions.map((attraction) => (
  <Link key={attraction.id} href={`/06_attractions/${attraction.id}`}>
    <div className="attraction-card">
      <h2>{attraction.name}</h2>
      <img src={attraction.coverimage} alt={attraction.name} width="200" />
    </div>
  </Link>
))}
javascript

ตอนนี้คลิกที่การ์ดใดก็ได้ จะพาไปยังหน้ารายละเอียดของสถานที่นั้น (/06_attractions/1, /06_attractions/2, …)

3.4 การนำทางด้วย useRouter#

บางครั้งต้องการเปลี่ยนหน้าจากภายในโค้ดหลังตรวจสอบเงื่อนไขบางอย่าง เช่น เปลี่ยนหน้าเมื่อล็อกอินสำเร็จเท่านั้น ซึ่ง Link ทำไม่ได้เพราะต้องรอผลการตรวจสอบก่อน กรณีแบบนี้ใช้ hook useRouter แล้วสั่ง router.push() จากในโค้ด

กลับไปแก้ไฟล์ app/04_form/page.js จากหัวข้อ 2.3 ให้เปลี่ยนไปยังหน้ารายการสถานที่เมื่อล็อกอินสำเร็จ — เพิ่ม import และแก้เฉพาะส่วนที่เปลี่ยน:

"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";

export default function LoginPage() {
  const router = useRouter();
  /* ...existing username / password / message state... */

  const handleSubmit = (event) => {
    event.preventDefault();
    if (username === "admin" && password === "admin") {
      router.push("/06_attractions"); // navigate only after the credentials check passes
    } else {
      setMessage("Invalid username or password");
    }
  };

  /* ...existing form JSX... */
}
javascript

กรอก admin / admin แล้วกด Login จะถูกพาไปยังหน้า /06_attractions ทันที แต่ถ้ากรอกผิดจะเห็นข้อความแจ้งเตือนแทน

วิธีใช้เมื่อ
<Link href="...">ให้ผู้ใช้คลิกลิงก์เปลี่ยนหน้าเอง
router.push("...")เปลี่ยนหน้าจากภายในโค้ด (เช่น หลังกดปุ่มหรือส่งฟอร์ม)

3.5 สารบัญ#

อัปเดต app/page.js เพื่อสร้างลิงก์ไปยังหน้าที่สร้างในบทความนี้:

import Link from "next/link";

export default function Home() {
  return (
    <div style={{ padding: "20px" }}>
      <h1>React & Next.js Lessons</h1>
      <ul>
        <li><Link href="/01_greeting">01 Greeting Component & Props</Link></li>
        <li><Link href="/02_card">02 Card List with map()</Link></li>
        <li><Link href="/03_product">03 useState Quantity Selector</Link></li>
        <li><Link href="/04_form">04 Login Form</Link></li>
        <li><Link href="/05_note">05 useEffect Auto-saved Note</Link></li>
        <li><Link href="/06_attractions">06 Fetch from API</Link></li>
      </ul>
    </div>
  );
}
javascript

เปิด http://localhost:3000 ↗ จะเห็นสารบัญที่ลิงก์ไปยังทุกหน้าในบทความ:

หน้าแรกแสดงสารบัญลิงก์ไปยังทุกหน้า


สรุป#

ส่วนที่ 1: Components และ Props#

  • แยก component เป็นไฟล์ในโฟลเดอร์ app/components/ เพื่อนำกลับมาใช้ซ้ำ
  • ส่งข้อมูลเข้า component ด้วย props (อ่านอย่างเดียว)
  • รวม map() กับ component เพื่อแสดงรายการ และใช้ children ส่งเนื้อหา JSX

ส่วนที่ 2: State และ Hooks#

  • ใส่ "use client" เมื่อ component ต้องมี state หรือ event
  • useState เก็บค่าที่เปลี่ยนแปลงได้ และเรนเดอร์ใหม่อัตโนมัติ
  • useEffect สั่งให้โค้ดทำงานตาม dependency array
  • รวม useState + useEffect + fetch เพื่อดึงข้อมูลจาก API พร้อมสถานะ loading

ส่วนที่ 3: Routing ใน Next.js#

  • โครงสร้างโฟลเดอร์ใน app/ กลายเป็น URL โดยอัตโนมัติ
  • Dynamic route [id] รับค่าจาก URL ผ่าน useParams()
  • Link สำหรับให้ผู้ใช้คลิก และ useRouter สำหรับเปลี่ยนหน้าจากในโค้ด

บทความถัดไป: การจัดการฟอร์มขั้นสูง การส่งข้อมูลไปยัง API (POST/PUT/DELETE) การยืนยันตัวตน (authentication) และการนำเว็บแอปขึ้นใช้งานจริง (deployment) จะอยู่ในบทความถัดไป

แหล่งข้อมูลเพิ่มเติม#

React & Next.js: Components, State, Hooks และ Routing
Author กานต์ ยงศิริวิทย์ / Karn Yongsiriwit
Published at September 21, 2026

Loading comments...

Comments 0