Components and Routing
Day 2 - ชั่วโมงที่ 4: แยกไฟล์ Component จริงใน Next.js
เป้าหมายของชั่วโมงนี้
หลังจบชั่วโมงที่สี่ของ Day 2 ผู้เรียนควรสามารถ:
- เห็นปัญหาของการเก็บ form, table, badge, type และ mock data ทั้งหมดไว้ใน
src/app/page.tsx - แยก UI ออกเป็นไฟล์ component จริงใน
src/components/ - สร้าง
StatusBadge.tsx,IssueList.tsxและIssueForm.tsxได้ - ใช้
exportและimportเพื่อเรียก component ข้ามไฟล์ได้ - ส่งข้อมูลเข้า component ผ่าน props ได้
- แยก
typesและdataเป็นไฟล์กลางเพื่อให้ component หลายตัวใช้ร่วมกันได้ - ใช้ component ที่แยกแล้วซ้ำใน route
/,/issuesและ/issues/new
ไฟล์ที่ใช้ในชั่วโมงนี้
ชั่วโมงนี้สอนการแยกไฟล์ component เป็นแกนหลัก โดยสร้างไฟล์จริงเหล่านี้ตั้งแต่ต้น:
src/components/StatusBadge.tsx
src/components/IssueList.tsx
src/components/IssueForm.tsx
src/types/issue.ts
src/data/issues.ts
src/app/page.tsx
src/app/layout.tsx
src/app/issues/page.tsx
src/app/issues/new/page.tsx
src/app/issues/[id]/page.tsxโครงสร้างเวลา 60 นาที
| เวลา | หัวข้อ | รูปแบบ |
|---|---|---|
| 0-5 นาที | Recap Hour 3: TypeScript data model | เชื่อมเข้าเนื้อหา |
| 5-12 นาที | แยกส่วนใน page.tsx ออกมาเป็นไฟล์ component อะไรบ้าง | Explain |
| 12-18 นาที | สร้างโครง src/components/, src/types/, src/data/ | Live coding |
| 18-35 นาที | สร้างไฟล์ component จริง: StatusBadge, IssueList, IssueForm | Live coding |
| 35-45 นาที | import component กลับมาใช้ใน src/app/page.tsx | Live coding |
| 45-52 นาที | ใช้ component เดิมซ้ำใน routes | Live coding |
| 52-60 นาที | ตรวจโค้ดสุดท้ายและ recap | สรุป |
Slide 1: Recap จากชั่วโมงที่ 3
ชั่วโมงที่แล้วเราทำอะไร
- สร้าง
IssueStatusและIssue - สร้าง mock data
issues: Issue[] - เปลี่ยน static table เป็น
issues.map(...) - ใช้
getStatusClass()เพื่อกำหนด class ของ status badge
ปัญหาตอนนี้
ทุกอย่างยังอยู่ใน src/app/page.tsx:
types
mock data
helper function
form JSX
table JSX
page layoutKey Message
ชั่วโมงนี้เราจะเริ่มจากการแยก form, table และ status badge ออกจาก page.tsx ไปเป็นไฟล์ component จริง
Slide 2: แยกอะไรออกจาก page.tsx บ้าง
จุดเริ่มต้น
ถ้า page.tsx มีทั้ง form, table, badge, type และ data อยู่รวมกัน จะเริ่มเจอปัญหา:
- ไฟล์ยาวและอ่านยาก
- หน้าอื่นใช้ form/table ซ้ำไม่ได้
- route
/issuesต้องเขียน table ใหม่ - route
/issues/newต้องเขียน form ใหม่ - แก้ UI จุดเดียว อาจต้องตามแก้หลายไฟล์
สิ่งที่จะแยกออกมา
flowchart LR
step0["status badge JSX"]
step1["src/components/StatusBadge.tsx"]
step0 --> step1Key Message
วันนี้ไม่แยกเป็น function ย่อยใน page.tsx แต่แยกเป็นไฟล์ component จริงเลย เพื่อให้ route อื่น import ไปใช้ซ้ำได้
Slide 3: โครงสร้างไฟล์หลังแยก Component
โครงที่ต้องการ
src/
app/
layout.tsx
page.tsx
issues/
page.tsx
new/
page.tsx
[id]/
page.tsx
components/
IssueForm.tsx
IssueList.tsx
StatusBadge.tsx
data/
issues.ts
types/
issue.tsแต่ละ folder ใช้ทำอะไร
| Folder | หน้าที่ |
|---|---|
components | เก็บ UI ที่ใช้ซ้ำ เช่น form, table, badge |
types | เก็บ TypeScript type |
data | เก็บ mock data |
app | เก็บ pages/routes |
Key Message
หัวใจของชั่วโมงนี้คือ src/components/ ส่วน types และ data เป็นไฟล์กลางที่ช่วยให้ component import ใช้ได้เป็นระเบียบ
Slide 4: Export และ Import คืออะไร
Component ที่อยู่คนละไฟล์ต้อง export ก่อน
export function IssueForm() {
return <form>...</form>;
}ไฟล์อื่น import ไปใช้
import { IssueForm } from "@/components/IssueForm";Named export
ในคอร์สนี้ใช้แบบนี้เป็นหลัก:
export function StatusBadge() {}
export function IssueList() {}
export function IssueForm() {}แล้ว import ด้วยชื่อใน { }:
import { StatusBadge } from "@/components/StatusBadge";Key Message
แยกไฟล์ component แล้วต้องเข้าใจ export และ import ไม่อย่างนั้น route อื่นจะเรียกใช้ component ไม่ได้
Slide 5: เตรียม Type กลางให้ Component ใช้ร่วมกัน
File
src/types/issue.tsตำแหน่งที่วาง
สร้าง folder src/types/ แล้วสร้างไฟล์ issue.ts
วาง type ที่เคยอยู่ด้านบนของ src/app/page.tsx มาไว้ที่ไฟล์นี้ เพื่อให้ StatusBadge และ IssueList import ไปใช้ได้
export type IssueStatus = "OPEN" | "IN_PROGRESS" | "DONE";
export type Issue = {
id: string;
reporterName: string;
reporterEmail: string;
title: string;
description: string;
adminComment?: string;
status: IssueStatus;
createdAt: string;
};จุดสำคัญ
ต้องใส่ export เพราะไฟล์อื่นจะ import type เหล่านี้ไปใช้
Slide 6: เตรียม Mock Data กลางให้หน้าและ Component ใช้ร่วมกัน
File
src/data/issues.tsตำแหน่งที่วาง
สร้าง folder src/data/ แล้วสร้างไฟล์ issues.ts
import type { Issue } from "@/types/issue";
export const issues: Issue[] = [
{
id: "001",
reporterName: "Anan",
reporterEmail: "anan@example.com",
title: "Login เข้าระบบไม่ได้",
description: "ไม่สามารถเข้าสู่ระบบด้วยบัญชีเดิมได้",
status: "OPEN",
createdAt: "2026-05-08",
},
{
id: "002",
reporterName: "Mali",
reporterEmail: "mali@example.com",
title: "ส่งแบบฟอร์มสมัครไม่ได้",
description: "กดส่งข้อมูลแล้วระบบขึ้น error",
status: "IN_PROGRESS",
createdAt: "2026-05-08",
},
{
id: "003",
reporterName: "Kanda",
reporterEmail: "kanda@example.com",
title: "ขอสิทธิ์เข้าใช้งาน dashboard",
description: "ต้องการสิทธิ์สำหรับตรวจสอบข้อมูลหลังบ้าน",
status: "DONE",
adminComment: "อนุมัติสิทธิ์และแจ้งผู้ใช้เรียบร้อยแล้ว",
createdAt: "2026-05-08",
},
];Key Message
issues เป็น mock data กลางที่หน้า / และ /issues ใช้ร่วมกันได้
Slide 7: สร้างไฟล์ Component StatusBadge.tsx
File
src/components/StatusBadge.tsxตำแหน่งที่วาง
สร้าง folder src/components/ แล้วสร้างไฟล์ StatusBadge.tsx
import type { IssueStatus } from "@/types/issue";
type StatusBadgeProps = {
status: IssueStatus;
};
function getStatusClass(status: IssueStatus): string {
if (status === "OPEN") {
return "status-open";
}
if (status === "IN_PROGRESS") {
return "status-progress";
}
return "status-done";
}
export function StatusBadge({ status }: StatusBadgeProps) {
return (
<span className={`status ${getStatusClass(status)}`}>
{status}
</span>
);
}ใช้งาน
<StatusBadge status={issue.status} />Key Message
StatusBadge รับแค่ status และรับผิดชอบเรื่องหน้าตาของ status เท่านั้น
Slide 8: Props คืออะไร
Props คือข้อมูลที่ส่งเข้า component
ตัวอย่าง:
<StatusBadge status="OPEN" />ใน component:
type StatusBadgeProps = {
status: IssueStatus;
};
export function StatusBadge({ status }: StatusBadgeProps) {
return <span>{status}</span>;
}ภาพจำง่าย ๆ
Parent component ส่งข้อมูล -> Child component รับผ่าน propsKey Message
Props ทำให้ component ไม่ต้องรู้ข้อมูลทั้งระบบ รู้เฉพาะข้อมูลที่จำเป็นต่อการแสดงผล
Slide 9: สร้างไฟล์ Component IssueList.tsx
File
src/components/IssueList.tsxตำแหน่งที่วาง
สร้างไฟล์ IssueList.tsx ใน src/components/
import Link from "next/link";
import { StatusBadge } from "@/components/StatusBadge";
import type { Issue } from "@/types/issue";
type IssueListProps = {
issues: Issue[];
};
export function IssueList({ issues }: IssueListProps) {
return (
<section aria-labelledby="issue-list-title">
<h2 id="issue-list-title">รายการปัญหาล่าสุด</h2>
<p>ตัวอย่างรายการปัญหาที่ถูกแจ้งเข้ามาในระบบ</p>
<div className="table-wrapper">
<table>
<thead>
<tr>
<th>รหัส</th>
<th>หัวข้อ</th>
<th>ผู้แจ้ง</th>
<th>สถานะ</th>
<th>รายละเอียด</th>
</tr>
</thead>
<tbody>
{issues.map((issue) => (
<tr key={issue.id}>
<td>#{issue.id}</td>
<td>{issue.title}</td>
<td>{issue.reporterName}</td>
<td>
<StatusBadge status={issue.status} />
</td>
<td>
<Link href={`/issues/${issue.id}`}>ดูรายละเอียด</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
);
}Key Message
IssueList ไม่สนใจว่า data มาจาก mock, state หรือ database แค่รับ issues แล้วแสดงผล
Slide 10: สร้างไฟล์ Component IssueForm.tsx
File
src/components/IssueForm.tsxตำแหน่งที่วาง
สร้างไฟล์ IssueForm.tsx ใน src/components/
export function IssueForm() {
return (
<section>
<h2>แจ้งปัญหาใหม่</h2>
<form>
<fieldset>
<legend>ข้อมูลปัญหา</legend>
<div className="form-row">
<div className="form-group">
<label htmlFor="reporterName">ชื่อผู้แจ้ง</label>
<input id="reporterName" name="reporterName" type="text" required />
</div>
<div className="form-group">
<label htmlFor="reporterEmail">อีเมลผู้แจ้ง</label>
<input id="reporterEmail" name="reporterEmail" type="email" required />
</div>
</div>
<div className="form-group">
<label htmlFor="title">หัวข้อปัญหา</label>
<input id="title" name="title" type="text" required />
</div>
<div className="form-group">
<label htmlFor="description">รายละเอียดปัญหา</label>
<textarea id="description" name="description" rows={5} required />
</div>
<button type="submit">ส่งข้อมูล</button>
</fieldset>
</form>
</section>
);
}Key Message
ตอนนี้ form ยังไม่ submit จริง แต่เรามี component ที่พร้อมต่อยอดใน Day 3 และ Day 4
Slide 11: Import Component กลับมาใช้ในหน้าแรก
File
src/app/page.tsxตำแหน่งที่แก้
ใช้ code นี้แทนเนื้อหาทั้งไฟล์ src/app/page.tsx
import { IssueForm } from "@/components/IssueForm";
import { IssueList } from "@/components/IssueList";
import { issues } from "@/data/issues";
export default function HomePage() {
return (
<>
<header>
<h1>ระบบแจ้งปัญหา IT</h1>
<p>แจ้งและติดตามปัญหาการใช้งานระบบภายใน</p>
</header>
<main>
<IssueForm />
<IssueList issues={issues} />
</main>
<footer>
<p>ฝ่ายเทคโนโลยีสารสนเทศ</p>
</footer>
</>
);
}ภาพที่ควรเห็น
page.tsx เหลือแค่โครงหน้าและ import สิ่งที่ต้องใช้ ไม่ต้องแบก form/table/data ทั้งหมดไว้ในไฟล์เดียว
Slide 12: Routing ใน Next.js App Router
Next.js ใช้ folder สร้าง route
flowchart LR
step0["src/app/page.tsx"]
step1["/"]
step0 --> step1Key Message
ใน App Router ถ้าอยากสร้างหน้าใหม่ ให้สร้าง folder แล้วใส่ไฟล์ page.tsx
Slide 13: สร้างหน้า /issues โดยใช้ IssueList
File
src/app/issues/page.tsxตำแหน่งที่วาง
สร้าง folder src/app/issues/ แล้วสร้างไฟล์ page.tsx
import { IssueList } from "@/components/IssueList";
import { issues } from "@/data/issues";
export default function IssuesPage() {
return (
<main>
<h1>รายการปัญหา</h1>
<IssueList issues={issues} />
</main>
);
}เปิดใน browser
http://localhost:3000/issuesSlide 14: สร้างหน้า /issues/new โดยใช้ IssueForm
File
src/app/issues/new/page.tsxตำแหน่งที่วาง
สร้าง folder src/app/issues/new/ แล้วสร้างไฟล์ page.tsx
import { IssueForm } from "@/components/IssueForm";
export default function NewIssuePage() {
return (
<main>
<h1>แจ้งปัญหาใหม่</h1>
<IssueForm />
</main>
);
}เปิดใน browser
http://localhost:3000/issues/newSlide 15: สร้าง Dynamic Route /issues/[id]
File
src/app/issues/[id]/page.tsxตำแหน่งที่วาง
สร้าง folder src/app/issues/[id]/ แล้วสร้างไฟล์ page.tsx
import { notFound } from "next/navigation";
import { issues } from "@/data/issues";
type IssueDetailPageProps = {
params: Promise<{
id: string;
}>;
};
export default async function IssueDetailPage({ params }: IssueDetailPageProps) {
const { id } = await params;
const issue = issues.find((item) => item.id === id);
if (!issue) {
notFound();
}
return (
<main>
<h1>{issue.title}</h1>
<p>{issue.description}</p>
<p>ผู้แจ้ง: {issue.reporterName}</p>
<p>อีเมล: {issue.reporterEmail}</p>
<p>สถานะ: {issue.status}</p>
</main>
);
}Key Message
[id] คือ dynamic segment ที่ทำให้ URL แต่ละรายการมีหน้ารายละเอียดของตัวเอง
Slide 16: เพิ่ม Navigation ใน layout.tsx
File
src/app/layout.tsxตำแหน่งที่แก้
เพิ่ม import Link from "next/link"; ด้านบน แล้ววาง <nav> ใน <body> ก่อน {children}
import Link from "next/link";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="th">
<body>
<nav className="app-nav" aria-label="เมนูหลัก">
<div className="app-nav-inner">
<Link href="/">หน้าแรก</Link>
<Link href="/issues">รายการปัญหา</Link>
<Link href="/issues/new">แจ้งปัญหาใหม่</Link>
</div>
</nav>
{children}
</body>
</html>
);
}Style สั้น ๆ สำหรับ Navigation
File:
src/app/globals.cssตำแหน่งที่วาง:
วางต่อท้ายไฟล์ src/app/globals.css
.app-nav {
border-bottom: 1px solid #e5e7eb;
background: #ffffff;
}
.app-nav-inner {
max-width: 960px;
margin: 0 auto;
padding: 12px 16px;
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.app-nav a {
color: #1f2937;
font-weight: 600;
text-decoration: none;
}
.app-nav a:hover {
color: #2563eb;
}Key Message
สิ่งที่อยู่ใน layout.tsx จะใช้ร่วมกันทุกหน้า ส่วน style ของ navigation ใส่สั้น ๆ ใน Day 2 แค่พอให้ใช้งานได้ แล้ว Day 3 ค่อยต่อยอดด้วย Tailwind
Slide 17: Import Path ที่ควรรู้
@/ คืออะไร
ตอนสร้าง project เราใช้ import alias:
@/* -> src/*ดังนั้น:
import { IssueList } from "@/components/IssueList";
import { issues } from "@/data/issues";
import type { Issue } from "@/types/issue";หมายถึง:
src/components/IssueList
src/data/issues
src/types/issueKey Message
ใช้ alias ช่วยให้ import สั้นและไม่ต้องเขียน path ยาว ๆ เช่น ../../../components/...
Slide 18: โค้ดสุดท้ายและโครงไฟล์หลัง Day 2
โครงไฟล์ที่ควรได้
src/
app/
layout.tsx
page.tsx
issues/
page.tsx
new/
page.tsx
[id]/
page.tsx
components/
IssueForm.tsx
IssueList.tsx
StatusBadge.tsx
data/
issues.ts
types/
issue.tsRoute ที่ต้องเปิดได้
flowchart LR
step0["/"]
step1["หน้าแรก"]
step0 --> step1Component ที่ถูกใช้ซ้ำ
flowchart LR
step0["IssueForm"]
step1["ใช้ใน / และ /issues/new"]
step0 --> step1Slide 19: ตรวจผลลัพธ์หลัง Day 2
สิ่งที่ต้องตรวจ
- หน้า
/เปิดได้ - หน้า
/issuesเปิดได้และเห็น list - หน้า
/issues/newเปิดได้และเห็น form - หน้า
/issues/001เปิดได้และเห็น detail - navigation กดไปแต่ละหน้าได้
page.tsxไม่ได้เก็บ type/data/component ทั้งหมดไว้ในไฟล์เดียวแล้ว
ถ้า error ให้ดู
- import path ถูกไหม
- export/import ชื่อตรงกันไหม
- สร้าง folder route ถูกที่ไหม
- ไฟล์ route ต้องชื่อ
page.tsx IssueListimportStatusBadgeแล้วหรือยัง
Slide 20: Common Mistakes
ข้อผิดพลาดที่พบบ่อย
- ลืม
exportในไฟล์ component - import แบบ default ทั้งที่ export แบบ named
- path ผิด เช่น
@/componentแทน@/components - ลืมสร้าง folder ก่อนสร้างไฟล์
- สร้าง route แล้วลืมชื่อไฟล์
page.tsx - ใช้
issuesใน route แต่ลืม import จาก@/data/issues - ใช้
<a>แทนLinkสำหรับ navigation ภายใน app - ลืมใส่
key={issue.id}ตอน.map()
Slide 21: Recap ชั่วโมงที่สี่ของ Day 2
สิ่งที่ได้เรียน
- Component ช่วยแบ่ง UI ให้ดูแลง่ายขึ้น
- Props คือข้อมูลที่ส่งเข้า component
StatusBadge,IssueList,IssueFormถูกแยกเป็นไฟล์ component จริง- Type และ mock data ถูกแยกไฟล์กลางเพื่อให้ component และ route ใช้ร่วมกัน
- App Router ใช้ folder และ
page.tsxสร้าง route - Dynamic route ใช้ folder เช่น
[id] layout.tsxใช้ใส่ UI ที่ทุกหน้าต้องมี เช่น navigation
ต่อไป
Day 3 จะใช้ component ที่แยกแล้วมาปรับ UI ด้วย Tailwind และเพิ่ม form state/validation
คำศัพท์สำคัญ
| คำศัพท์ | ความหมาย |
|---|---|
| Component | ชิ้นส่วน UI ที่แยกเป็น function และใช้ซ้ำได้ |
| Props | ข้อมูลที่ส่งเข้า component |
| Named export | การ export แบบ export function ... |
| Import alias | path ย่อ เช่น @/components/... |
| Route | URL path ของหน้าเว็บ |
| App Router | ระบบ routing แบบ folder-based ของ Next.js |
| Dynamic route | route ที่มี parameter เช่น /issues/[id] |
Link | component ของ Next.js สำหรับ navigation ภายใน app |