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 เชื่อมต่อกับข้อมูลภายนอก
  3. ส่วนที่ 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+`)

เปิด 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 ในโปรเจกต์#

ก่อนเริ่มสร้าง 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

Hello World หลังใส่ CSS

หมายเหตุ: บางตัวอย่างใช้ 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 ตัวเดียวกันแสดงผลซ้ำสองครั้ง:

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

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

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

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

หน้า /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"ส่งข้อความเป็น prop ด้วยเครื่องหมายคำพูด
count={5}ใช้ {} ส่งค่าจากนิพจน์ JavaScript เช่น ตัวเลข ตัวแปร หรือออบเจกต์

หมายเหตุ: props เป็นข้อมูลแบบ อ่านอย่างเดียว (read-only) สำหรับ component ที่รับมา หากต้องการเก็บข้อมูลที่เปลี่ยนจากการโต้ตอบของผู้ใช้ ให้ใช้ state ซึ่งจะอธิบายในส่วนที่ 2

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

เมื่อมี component Card แล้ว เราสามารถใช้ map() แปลงข้อมูลแต่ละรายการในอาร์เรย์เป็นการ์ด โดยส่งชื่อและคำอธิบายของแต่ละรายการผ่าน props

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 โดยใช้ id เป็น key ที่คงที่และไม่ซ้ำกันในรายการ เพื่อให้ React ระบุการ์ดแต่ละใบได้เมื่อข้อมูลเปลี่ยนแปลง

เปิด http://localhost:3000/02_card ↗ จะเห็นข้อมูลแต่ละหลักสูตรแสดงด้วย component Card ตามภาพ:

หน้า /02_card แสดงรายการ Courses ด้วย 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 อีกครั้ง

useState re-render loop diagram

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

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

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

หน้า /04_form ฟอร์มล็อกอินที่แสดง 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 อัปเดตหน้าเว็บแล้ว

useEffect localStorage diagram

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

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

Dependency array คืออาร์เรย์ที่ส่งเป็นอาร์กิวเมนต์ตัวที่สองของ useEffect ใช้ระบุค่าที่ effect อ้างอิงและต้องติดตาม เมื่อค่าเหล่านี้เปลี่ยน React จะเรียก effect อีกครั้ง

Dependency arrayeffect ทำงานเมื่อ
[] (ว่าง)ทำงานเมื่อ 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รายละเอียดของสถานที่
coverimageURL ของรูปภาพหน้าปก
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>
  );
}
javascript

Responsive ด้วย 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;
}
css

display: flex และ flex-wrap: wrap ช่วยให้การ์ดขึ้นแถวใหม่เมื่อพื้นที่ไม่พอ ส่วน gap กำหนดระยะห่างระหว่างการ์ด เราใช้ border-radius และ box-shadow เพิ่มมุมโค้งและเงา พร้อมกำหนดกรอบรูปให้มีขนาดเท่ากัน โดย object-fit: cover จะครอบตัดภาพให้เต็มกรอบและรักษาสัดส่วนเดิม สำหรับชื่อสถานที่ ใช้ white-space, overflow และ 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([])เตรียม 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/ ตามที่เราใช้สร้างหน้าตัวอย่างมาตลอดบทความ

Next.js file-based routing diagram

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 ตามเส้นทางที่เปิดอยู่

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

ขั้นต่อไปคือใช้ 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 ↗ จะเห็นหน้ารายละเอียดพร้อมลิงก์กลับ:

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

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

สำหรับการนำทางระหว่างหน้าภายในแอป 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)

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

React & Next.js: Components, State, Hooks และ Routing
ผู้เขียน กานต์ ยงศิริวิทย์ / Karn Yongsiriwit
เผยแพร่เมื่อ September 21, 2026
ลิขสิทธิ์ CC BY-NC-SA 4.0

กำลังโหลดความคิดเห็น...

ความคิดเห็น 0