React & Next.js: Components, State, Hooks และ Routing
เรียนรู้การสร้างเว็บแอปด้วย React และ Next.js ตั้งแต่แยก component และส่ง props ไปจนถึงจัดการ state สร้างฟอร์ม ดึงข้อมูลจาก API และเชื่อมหน้าเว็บด้วย routing
บทความนี้จะพาคุณสร้างเว็บแอปพลิเคชันด้วย 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เชื่อมต่อกับข้อมูลภายนอก - ส่วนที่ 3 — Routing ใน Next.js: กำหนดเส้นทางด้วยโครงสร้างไฟล์ สร้าง dynamic routes และเปลี่ยนหน้าด้วย
LinkกับuseRouter
ข้อกำหนดเบื้องต้น: ควรมีพื้นฐาน HTML, CSS และ JavaScript พร้อมติดตั้ง Node.js กับ VS Code ตามหัวข้อถัดไป ส่วนตัวอย่างการดึงข้อมูลจะใช้ Me Live Code API ↗
การเตรียมเครื่องมือ#
ก่อนสร้างโปรเจกต์ ให้เตรียมเครื่องมือสองอย่างต่อไปนี้ให้พร้อม
- VS Code — โปรแกรมสำหรับเขียนโค้ด ติดตั้งจาก code.visualstudio.com ↗
- Node.js (LTS) — สภาพแวดล้อมสำหรับรัน JavaScript นอกเบราว์เซอร์ ติดตั้งจาก nodejs.org ↗
จากนั้นเปิด Terminal แล้วสร้างโปรเจกต์ Next.js ชื่อ react-nextjs-101 โดยใช้ --js เพื่อเลือก JavaScript สำหรับตัวอย่างในบทความ:
# สร้างโปรเจกต์ 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 ในโปรเจกต์#
ก่อนเริ่มสร้าง component เราจะเตรียมสไตล์พื้นฐานให้โปรเจกต์ โดยใช้ global stylesheet หรือไฟล์ CSS ที่ใช้ร่วมกันทั้งแอป สร้างไฟล์ .css แล้ว import ใน app/layout.js เพื่อให้ทุกหน้าใช้กฎ CSS ชุดเดียวกัน
สร้างไฟล์ 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} เราจึงนำเข้า CSS ที่ใช้ร่วมกันไว้ในไฟล์นี้ได้ นอกจากนี้ยังใช้ metadata กำหนด title ที่แสดงบนแท็บเบราว์เซอร์ และ description ที่อธิบายเนื้อหาของเว็บไซต์
แก้ไข 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 ซึ่งเป็นหน้าแรกของโปรเจกต์ ให้แสดงข้อความ Hello World แล้วเปิด http://localhost:3000 ↗ เพื่อตรวจดูผลของ CSS
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 เพื่อบอกว่าต้องการแสดงอะไรบนหน้าเว็บ การ แยก 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 มาใช้ โดยพาธ ../components/ หมายถึงย้อนจากโฟลเดอร์ 01_greeting ขึ้นไปที่ app แล้วเข้าโฟลเดอร์ components:
import Greeting from "../components/Greeting";
export default function GreetingPage() {
return (
<div>
<h1>My App</h1>
<Greeting />
<Greeting />
</div>
);
}javascriptเราสามารถเขียน <Greeting /> ใน JSX ได้หลายตำแหน่ง แต่ละตำแหน่งจะแสดงเนื้อหาจาก component เดียวกัน จึงแก้ไขข้อความหรือโครงสร้างได้จากไฟล์ Greeting.js เพียงแห่งเดียว
เปิด http://localhost:3000/01_greeting ↗ จะเห็น Greeting ตัวเดียวกันแสดงผลซ้ำสองครั้ง:

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

แก้ไขไฟล์ 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 ↗ อีกครั้ง จะเห็นคำทักทายที่เปลี่ยนไปตามค่า name ของแต่ละ component:

ตัวอย่าง: 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" | ส่งข้อความเป็น prop ด้วยเครื่องหมายคำพูด |
count={5} | ใช้ {} ส่งค่าจากนิพจน์ JavaScript เช่น ตัวเลข ตัวแปร หรือออบเจกต์ |
หมายเหตุ: props เป็นข้อมูลแบบ อ่านอย่างเดียว (read-only) สำหรับ component ที่รับมา หากต้องการเก็บข้อมูลที่เปลี่ยนจากการโต้ตอบของผู้ใช้ ให้ใช้ state ซึ่งจะอธิบายในส่วนที่ 2
1.4 การแสดงรายการด้วย Props และ map()#
เมื่อมี component Card แล้ว เราสามารถใช้ map() แปลงข้อมูลแต่ละรายการในอาร์เรย์เป็นการ์ด โดยส่งชื่อและคำอธิบายของแต่ละรายการผ่าน props

สร้างไฟล์ 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 โดยใช้ id เป็น key ที่คงที่และไม่ซ้ำกันในรายการ เพื่อให้ React ระบุการ์ดแต่ละใบได้เมื่อข้อมูลเปลี่ยนแปลง
เปิด http://localhost:3000/02_card ↗ จะเห็นข้อมูลแต่ละหลักสูตรแสดงด้วย component Card ตามภาพ:

1.5 children Prop#
นอกจาก props อย่าง title และ description แล้ว เรายังส่ง เนื้อหา JSX ที่เขียนซ้อนอยู่ภายในแท็กของ component ผ่าน prop ชื่อ children ได้ เหมาะกับ component ที่ทำหน้าที่ครอบเนื้อหา เช่น กล่อง Panel
สร้างไฟล์ 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 และแสดงตรงตำแหน่ง {children} ภายใน component:
<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 title และ description เพื่อแสดงข้อมูลแต่ละหลักสูตร เราจึงเลือกวิธีส่งข้อมูลให้เหมาะกับหน้าที่ของแต่ละ component ได้
ส่วนที่ 2: State และ Hooks#
ตัวอย่างที่ผ่านมาแสดงข้อมูลที่กำหนดไว้ในโค้ด ต่อไปเราจะทำให้หน้าเว็บ โต้ตอบกับผู้ใช้ได้ เช่น เพิ่มจำนวนสินค้าและรับข้อความจากฟอร์ม โดยใช้ Hooks ซึ่งเป็นฟังก์ชันของ React ที่มีชื่อขึ้นต้นด้วย use
2.1 Server Component และ Client Component#
ใน Next.js App Router หน้าเว็บและ layout เป็น Server Component โดยค่าเริ่มต้น เหมาะกับการเตรียมและแสดงเนื้อหาฝั่งเซิร์ฟเวอร์ ส่วนการใช้ state หรือรับ event จากผู้ใช้ เช่น การคลิกและการพิมพ์ ต้องอยู่ใน Client Component
สำหรับตัวอย่างที่ใช้ useState, useEffect หรือ event handler ให้ใส่ "use client" ไว้ตอนต้นของไฟล์ก่อน import เพื่อกำหนดจุดเริ่มต้นของส่วนที่ทำงานฝั่ง client
"use client";javascriptหมายเหตุ: ไม่จำเป็นต้องใส่
"use client"ในทุกไฟล์ ให้กำหนดขอบเขตตรงส่วนที่ต้องโต้ตอบกับผู้ใช้ โดย component ที่ import เข้ามาภายในขอบเขตนี้จะเป็นส่วนหนึ่งของฝั่ง client ด้วย
2.2 useState — การเก็บค่าที่เปลี่ยนแปลงได้#
useState ช่วยให้ component จดจำข้อมูลระหว่างการแสดงผลแต่ละครั้ง เช่น จำนวนสินค้าที่ผู้ใช้เลือก เมื่ออัปเดต state ผ่านฟังก์ชัน setter React จะนำค่าใหม่มาใช้เรนเดอร์ component อีกครั้ง

เราจะทดลองด้วย ช่องเลือกจำนวนสินค้า เมื่อผู้ใช้กดปุ่มเพิ่มหรือลดจำนวน จะอัปเดต state ชื่อ quantity แล้วนำจำนวนล่าสุดไปคำนวณราคารวมในการเรนเดอร์ครั้งถัดไป
สร้างไฟล์ 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 ผ่านฟังก์ชัน setter เช่น
setQuantityเสมอ เพื่อให้ React รับรู้การเปลี่ยนแปลง การกำหนดค่าให้ตัวแปรโดยตรง เช่นquantity = quantity + 1ไม่ใช่วิธีอัปเดต state
2.3 การจัดการ Event และ Controlled Input#
เราสามารถใช้ state ควบคุมช่องรับข้อความได้เช่นกัน โดยส่งค่าปัจจุบันให้ value และใช้ onChange อัปเดต state เมื่อผู้ใช้พิมพ์ รูปแบบนี้เรียกว่า controlled input ตัวอย่างต่อไปเป็น ฟอร์มล็อกอิน ที่เก็บ username และ password ไว้ใน state แล้วตรวจสอบเมื่อส่งฟอร์ม
สร้างไฟล์ 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 | กำหนดฟังก์ชันที่ทำงานเมื่อส่งฟอร์ม เช่น กดปุ่ม Login หรือกด Enter ในช่องกรอก |
event.preventDefault() | ยกเลิกการส่งฟอร์มตามพฤติกรรมปกติของเบราว์เซอร์ เพื่อจัดการข้อมูลด้วยโค้ดของเรา |
username === "admin" && password === "admin" | ตรวจว่าค่าทั้งสองตรงกับข้อมูลตัวอย่างหรือไม่ |
หมายเหตุ: การตรวจสอบ
admin/adminในตัวอย่างนี้ทำฝั่ง client เพื่อสาธิตเท่านั้น ระบบจริงต้องยืนยันตัวตนที่เซิร์ฟเวอร์
2.4 useEffect — การทำงานเมื่อ Component เปลี่ยนแปลง#
เมื่อ component ต้องเชื่อมต่อกับสิ่งภายนอก React เช่น พื้นที่เก็บข้อมูลของเบราว์เซอร์ ตัวจับเวลา หรือ API เราสามารถใช้ useEffect กำหนดงานที่จะทำหลังจาก React อัปเดตหน้าเว็บแล้ว

ตัวอย่างนี้เป็น ช่องกรอกบันทึกที่บันทึกอัตโนมัติ โดยใช้ effect หนึ่งอ่านข้อความเดิมจาก localStorage เมื่อเปิดหน้า และใช้อีก effect เขียนข้อความกลับเมื่อ state note เปลี่ยน เพื่อเก็บบันทึกไว้ในเบราว์เซอร์
สร้างไฟล์ 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 อ้างอิงและต้องติดตาม เมื่อค่าเหล่านี้เปลี่ยน React จะเรียก effect อีกครั้ง
| Dependency array | effect ทำงานเมื่อ |
|---|---|
[] (ว่าง) | ทำงานเมื่อ component ถูกนำมาแสดง โดยในโหมดพัฒนาที่เปิด Strict Mode อาจมีรอบทดสอบเพิ่มเติม |
[value] | ทำงานเมื่อ component ถูกนำมาแสดง และหลังการอัปเดตที่ value เปลี่ยน |
| ไม่ใส่เลย | ทำงานหลังทุกครั้งที่ React อัปเดต component บนหน้าเว็บ |
2.5 การดึงข้อมูลจาก API#
ต่อไปเราจะใช้ useEffect เริ่มดึงข้อมูล และใช้ useState เก็บผลลัพธ์เพื่อนำมาแสดงบนหน้าเว็บ ตัวอย่างนี้ใช้ Attractions API จาก Me Live Code API ↗ เพื่อสร้างหน้ารายการสถานที่ท่องเที่ยว
ข้อมูลที่ API ส่งกลับมาอยู่ในรูปแบบ JSON (JavaScript Object Notation) ซึ่งเป็นข้อความสำหรับแลกเปลี่ยนข้อมูลระหว่างแอปกับเซิร์ฟเวอร์ โครงสร้างที่เราจะพบในตัวอย่างมีทั้ง ออบเจกต์ ซึ่งใช้ {} เก็บคู่ "key": value และ อาร์เรย์ ซึ่งใช้ [] เก็บข้อมูลหลายรายการ ค่าแต่ละตำแหน่งอาจเป็นข้อความ ตัวเลข true, false, null หรือออบเจกต์และอาร์เรย์ที่ซ้อนกันได้
ก่อนเขียนโค้ด ลองเปิด https://melivecode.com/api/attractions ↗ ในเบราว์เซอร์เพื่อดูโครงสร้างข้อมูล ตัวอย่างด้านล่างแสดงสถานที่ท่องเที่ยว 2 รายการในรูปแบบ อาร์เรย์ของออบเจกต์:
[
{
"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 | วันเวลาที่เพิ่มข้อมูล |
เมื่ออ่าน JSON เป็นข้อมูล JavaScript แล้ว เราจะได้อาร์เรย์ที่เก็บลง state ด้วย setAttractions(data) ได้ จากนั้นใช้ map() สร้างการ์ดแสดงสถานที่แต่ละรายการ เช่นเดียวกับการแสดงหลักสูตรในส่วนที่ 1
สร้างไฟล์ 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 — เราจะจัดการ์ดให้ขึ้นแถวใหม่ตามพื้นที่หน้าจอ โดยเพิ่มสไตล์ของรายการและการ์ดต่อท้ายไฟล์ 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 จะครอบตัดภาพให้เต็มกรอบและรักษาสัดส่วนเดิม สำหรับชื่อสถานที่ ใช้ white-space, overflow และ 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([]) | เตรียม state สำหรับเก็บรายการสถานที่ โดยเริ่มจากอาร์เรย์ว่าง |
useState(true) | เตรียมสถานะ loading เพื่อแสดงว่ากำลังรอข้อมูล |
fetch(url) | ส่งคำขอข้อมูลไปยัง API |
.then(res => res.json()) | อ่านเนื้อหา JSON ใน response และแปลงเป็นข้อมูล JavaScript |
setAttractions(data) | อัปเดต state ด้วยข้อมูลที่ได้รับ เพื่อให้ React แสดงรายการใหม่ |
หมายเหตุ: state
loadingใช้แยกช่วงที่กำลังรอข้อมูลออกจากช่วงที่พร้อมแสดงรายการ เมื่อได้รับข้อมูลแล้วจึงเรียกsetLoading(false)เพื่อแสดงการ์ด
ส่วนที่ 3: Routing ใน Next.js#
เมื่อมีหน้ารายการสถานที่แล้ว เราจะเพิ่มหน้ารายละเอียดและเชื่อมทั้งสองหน้าเข้าด้วยกัน Next.js ใช้ระบบ file-based routing ซึ่งกำหนดเส้นทางจากโครงสร้างโฟลเดอร์และไฟล์ page.js ภายใต้ app/ ตามที่เราใช้สร้างหน้าตัวอย่างมาตลอดบทความ

3.1 File-based Routing#
ในโครงสร้างตัวอย่างนี้ ตำแหน่งของไฟล์ page.js จะกำหนด URL ของหน้าเว็บ โดยชื่อโฟลเดอร์แต่ละชั้นเป็นส่วนหนึ่งของเส้นทาง ดังตาราง
| ตำแหน่งไฟล์ | 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 เป็นค่าที่เปลี่ยนได้ หรือ route parameter
เริ่มจากอ่านค่าใน URL ก่อน โดยสร้างไฟล์ app/06_attractions/[id]/page.js แล้วใช้ useParams() นำ id มาแสดงบนหน้าเว็บ:
"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 ทั้งสอง URL ใช้ไฟล์หน้าเว็บเดียวกัน แต่ useParams() จะคืนค่า id ตามเส้นทางที่เปิดอยู่

ขั้นต่อไปคือใช้ id เลือกดึงข้อมูล ทีละรายการ ผ่าน endpoint เช่น https://melivecode.com/api/attractions/1 ↗ หรือ https://melivecode.com/api/attractions/2 ↗ โดยผลลัพธ์สำหรับหน้ารายละเอียดมีโครงสร้างเป็นออบเจกต์ { status, attraction } ซึ่งเก็บข้อมูลสถานที่ไว้ในฟิลด์ 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 | สถานะที่ API ส่งกลับมา โดย "ok" หมายถึงสำเร็จ |
attraction | ออบเจกต์ของสถานที่ที่เลือก มีฟิลด์ตามที่อธิบายในหัวข้อ 2.5 |
เมื่ออ่านผลลัพธ์เป็นตัวแปร data แล้ว เราจะใช้ data.attraction เพื่อเข้าถึงออบเจกต์สถานที่และนำไปเก็บใน state
กลับไปแก้ไข app/06_attractions/[id]/page.js ให้นำ id ไปประกอบ URL ของ 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โค้ดนี้ใช้ id เลือกข้อมูลจาก GET /api/attractions/{id} แล้วเก็บ data.attraction ไว้สำหรับแสดงผล ส่วน Link ด้านบนใช้กลับไปยังหน้ารายการ เราจะนำ Link ไปเชื่อมการ์ดแต่ละใบกับหน้ารายละเอียดในหัวข้อถัดไป
เปิด http://localhost:3000/06_attractions/1 ↗ จะเห็นหน้ารายละเอียดพร้อมลิงก์กลับ:

| ส่วน | คำอธิบาย |
|---|---|
[id] | ชื่อโฟลเดอร์แบบ dynamic — รับค่าจาก URL |
useParams() | hook ที่คืนค่า parameter จาก URL |
[id] ใน dependency | ดึงข้อมูลใหม่เมื่อ id เปลี่ยน |
3.3 การนำทางด้วย Link#
สำหรับการนำทางระหว่างหน้าภายในแอป Next.js เราจะใช้ component Link จาก next/link เพื่อให้เปลี่ยนหน้าได้โดยไม่ต้องโหลดเอกสารทั้งหน้าใหม่
แก้ไข app/06_attractions/page.js จากหัวข้อ 2.5 โดยเพิ่ม import Link แล้วปรับส่วน map() ให้การ์ดแต่ละใบเชื่อมไปยังหน้ารายละเอียดตาม id ของสถานที่:
"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#
หากต้องการเปลี่ยนหน้าหลังทำงานบางอย่างเสร็จ เช่น ตรวจสอบข้อมูลในฟอร์มผ่านแล้ว เราสามารถใช้ hook useRouter และเรียก router.push() ภายใน event handler ได้ วิธีนี้ช่วยกำหนดจังหวะการเปลี่ยนหน้าจากเงื่อนไขในโค้ด
กลับไปที่ฟอร์มตัวอย่างใน app/04_form/page.js จากหัวข้อ 2.3 แล้วเพิ่ม useRouter เพื่อพาไปยังหน้ารายการสถานที่เมื่อข้อมูลตรงตามเงื่อนไข โดยปรับเฉพาะส่วนที่แสดงด้านล่าง:
"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 แม่ไปยัง component ลูกผ่าน props ซึ่งเป็นข้อมูลแบบอ่านอย่างเดียวสำหรับผู้รับ
- ใช้
map()สร้าง component จากรายการข้อมูล และใช้childrenส่งเนื้อหาที่เขียนซ้อนภายในแท็ก
ส่วนที่ 2: State และ Hooks#
- ใช้
"use client"กำหนดขอบเขตของส่วนที่ต้องใช้ state, effect หรือ event handler - ใช้
useStateเก็บข้อมูล และอัปเดตผ่าน setter เพื่อให้ React แสดงผลตามค่าใหม่ - ใช้
useEffectเชื่อมต่อกับสิ่งภายนอก React โดยระบุค่าที่ต้องติดตามใน dependency array - ใช้
fetchดึงข้อมูลใน effect แล้วเก็บผลลัพธ์และสถานะ loading ไว้ใน state
ส่วนที่ 3: Routing ใน Next.js#
- กำหนด URL ของแต่ละหน้าด้วยโครงสร้างโฟลเดอร์และไฟล์
page.jsภายในapp/ - ใช้ dynamic route
[id]ร่วมกับuseParams()เพื่อแสดงข้อมูลตามค่าที่อยู่ใน URL - ใช้
Linkสร้างลิงก์ระหว่างหน้า และใช้useRouterเมื่อต้องสั่งเปลี่ยนหน้าจากโค้ด
บทความถัดไป: เราจะต่อยอดด้วยการจัดการฟอร์มที่ซับซ้อนขึ้น การส่งข้อมูลไปยัง API ด้วย POST/PUT/DELETE การยืนยันตัวตน (authentication) และการนำเว็บแอปขึ้นใช้งานจริง (deployment)