React & Next.js: Components, State, Hooks และ Routing
สร้างเว็บแอปด้วย React และ Next.js ตั้งแต่แยก component, ส่ง props, จัดการ state ด้วย hooks, ทำฟอร์มล็อกอิน, ดึงข้อมูลจาก API และทำ routing แบบ file-based
บทความนี้แนะนำการสร้างเว็บแอปพลิเคชันด้วย React และ Next.js ตั้งแต่พื้นฐาน โดยสร้างโปรเจกต์ใหม่ชื่อ react-nextjs-101 และเรียนรู้แนวคิดหลักที่ใช้ทำงานจริง ได้แก่ การแยก component, การส่งข้อมูลด้วย props, การจัดการ state ด้วย hooks, การดึงข้อมูลจาก API และการทำ routing
เนื้อหาแบ่งเป็น 3 ส่วนที่ต่อยอดกัน:
- ส่วนที่ 1 — Components และ Props: แยก component เป็นไฟล์ ส่งข้อมูลด้วย props และแสดงรายการด้วย
map() - ส่วนที่ 2 — State และ Hooks:
useState, การจัดการ event,useEffectและการดึงข้อมูลจาก API - ส่วนที่ 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+`)

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

3. รัน npm run dev — พิมพ์คำสั่งต่อไปนี้แล้วกด Enter
npm run devbashเมื่อ server พร้อมใช้งานจะแสดงลิงก์ Local: http://localhost:3000 กด Ctrl ค้างแล้วคลิกที่ลิงก์ VS Code จะเปิดเบราว์เซอร์ในตัว (Simple Browser) แสดงหน้าแอปขึ้นมาทันทีโดยไม่ต้องออกจาก 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
หมายเหตุ: ตัวอย่างในบทความนี้ยังใช้ 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>
);
}javascriptGreeting ถูกเรียกใช้ได้หลายครั้งเหมือนแท็ก HTML ทั่วไป นี่คือหัวใจของการนำ component กลับมาใช้ซ้ำ
เปิด http://localhost:3000/01_greeting ↗ จะเห็น Greeting ตัวเดียวกันแสดงผลซ้ำสองครั้ง:

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

แก้ไขไฟล์ 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 ↗ จะเห็นผลลัพธ์แบบนี้:

ตัวอย่าง: 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 เพื่อแสดงรายการข้อมูลเป็นการ์ดหลายใบ

สร้างไฟล์ 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 ↗ จะเห็นผลลัพธ์แบบนี้:

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>
);
}javascriptPanel รับเนื้อหาผ่าน 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 ใหม่ให้อัตโนมัติ

ตัวอย่างที่ใช้ได้จริงคือ ช่องเลือกจำนวนสินค้า ในหน้าสั่งซื้อ — เมื่อผู้ใช้กดเพิ่ม/ลดจำนวน 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 ↗ แล้วกดปุ่ม + / − จะเห็นจำนวนและราคารวมเปลี่ยนทันที:

| ส่วน | คำอธิบาย |
|---|---|
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:

| ส่วน | คำอธิบาย |
|---|---|
value={username} / value={password} | ผูกค่าของแต่ละ input กับ state |
onChange | อัปเดต state ทุกครั้งที่ค่าใน input เปลี่ยน |
onSubmit | event ที่ทำงานเมื่อกดปุ่ม Login หรือกด Enter |
event.preventDefault() | กันไม่ให้เบราว์เซอร์รีโหลดหน้าตอน submit ฟอร์ม |
username === "admin" && password === "admin" | เงื่อนไขตรวจสอบล็อกอิน |
หมายเหตุ: การตรวจสอบ
admin/adminในตัวอย่างนี้ทำฝั่ง client เพื่อสาธิตเท่านั้น ระบบจริงต้องยืนยันตัวตนที่เซิร์ฟเวอร์
2.4 useEffect — การทำงานเมื่อ Component เปลี่ยนแปลง#
useEffect ใช้สั่งให้โค้ดทำงานหลังจาก component เรนเดอร์ เช่น การตั้งเวลา การดึงข้อมูล หรือการทำงานกับสิ่งภายนอก React

ตัวอย่างที่ใช้ได้จริงคือ ช่องกรอกบันทึกที่เซฟอัตโนมัติ — โหลดข้อความเดิมจาก 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 ↗ พิมพ์ข้อความแล้วรีเฟรชหน้า ข้อความจะยังอยู่:

Dependency array (อาร์เรย์ตัวที่สองของ useEffect) เป็นตัวกำหนดว่า effect จะทำงานเมื่อใด
| Dependency array | effect ทำงานเมื่อ |
|---|---|
[] (ว่าง) | ทำงานครั้งเดียวตอน 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 | รายละเอียดของสถานที่ |
coverimage | URL ของรูปภาพหน้าปก |
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>
);
}javascriptResponsive ด้วย 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;
}cssdisplay: flex กับ flex-wrap: wrap ทำให้การ์ดที่เกินตกลงไปขึ้นแถวใหม่เองเมื่อจอแคบลง ส่วน gap เว้นระยะห่างระหว่างการ์ด มุมโค้ง (border-radius) และเงา (box-shadow) ช่วยให้การ์ดดูนุ่มนวลขึ้น และ object-fit: cover ทำให้รูปทุกใบมีขนาดเท่ากันโดยไม่ผิดสัดส่วน ส่วนชื่อสถานที่ตัดด้วย text-overflow: ellipsis เมื่อยาวเกินหนึ่งบรรทัดจะแสดงเป็น ... แทน การ์ดจึงสูงเท่ากันทุกใบ
เปิด http://localhost:3000/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 เอง

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() โดยอัตโนมัติ ไม่ว่าจะใส่เลขใดก็เข้าหน้าเดียวกันนี้

นอกจาก 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>
);
}javascriptGET /api/attractions/{id} คืนข้อมูลรายตัวในรูปแบบ { status, attraction } จึงอ่านค่าจาก data.attraction ส่วน Link ด้านบนพากลับไปหน้ารายการ (รายละเอียดของ Link อยู่ในหัวข้อ 3.3)
เปิด http://localhost:3000/06_attractions/1 ↗ จะเห็นหน้ารายละเอียดพร้อมลิงก์กลับ:

| ส่วน | คำอธิบาย |
|---|---|
[id] | ชื่อโฟลเดอร์แบบ dynamic — รับค่าจาก URL |
useParams() | hook ที่คืนค่า parameter จาก URL |
[id] ใน dependency | ดึงข้อมูลใหม่เมื่อ id เปลี่ยน |
3.3 การนำทางด้วย Link#
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) จะอยู่ในบทความถัดไป