Day 2 / Hour 4

Components and Routing

Component extraction, shared types, mock data files, and basic routes.

60 minutes
The app uses reusable components and routes.

Components and Routing

Day 2 - ชั่วโมงที่ 4: แยกไฟล์ Component จริงใน Next.js

เป้าหมายของชั่วโมงนี้

หลังจบชั่วโมงที่สี่ของ Day 2 ผู้เรียนควรสามารถ:

  1. เห็นปัญหาของการเก็บ form, table, badge, type และ mock data ทั้งหมดไว้ใน src/app/page.tsx
  2. แยก UI ออกเป็นไฟล์ component จริงใน src/components/
  3. สร้าง StatusBadge.tsx, IssueList.tsx และ IssueForm.tsx ได้
  4. ใช้ export และ import เพื่อเรียก component ข้ามไฟล์ได้
  5. ส่งข้อมูลเข้า component ผ่าน props ได้
  6. แยก types และ data เป็นไฟล์กลางเพื่อให้ component หลายตัวใช้ร่วมกันได้
  7. ใช้ 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, IssueFormLive coding
35-45 นาทีimport component กลับมาใช้ใน src/app/page.tsxLive coding
45-52 นาทีใช้ component เดิมซ้ำใน routesLive 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 layout

Key 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 --> step1

Key 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 รับผ่าน props

Key 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 --> step1

Key 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/issues

Slide 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/new

Slide 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/issue

Key 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.ts

Route ที่ต้องเปิดได้

flowchart LR
  step0["/"]
  step1["หน้าแรก"]
  step0 --> step1

Component ที่ถูกใช้ซ้ำ

flowchart LR
  step0["IssueForm"]
  step1["ใช้ใน / และ /issues/new"]
  step0 --> step1

Slide 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
  • IssueList import StatusBadge แล้วหรือยัง

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 aliaspath ย่อ เช่น @/components/...
RouteURL path ของหน้าเว็บ
App Routerระบบ routing แบบ folder-based ของ Next.js
Dynamic routeroute ที่มี parameter เช่น /issues/[id]
Linkcomponent ของ Next.js สำหรับ navigation ภายใน app