Mock CRUD and Database Prep
Day 3 - ชั่วโมงที่ 4: เปลี่ยนสถานะใน Mock State
เป้าหมายของชั่วโมงนี้
หลังจบชั่วโมงนี้ นักศึกษาจะสามารถ:
- อธิบาย Create, Read และ Update ในโปรเจกต์ปัจจุบันได้
- เปลี่ยน Status ของ Issue ใน State ได้
- Update Array และ Object แบบไม่แก้ค่าเดิมโดยตรง
- ส่ง Event Handler จาก Parent ไปยัง Child ผ่าน Props
- ทำให้
IssueListใช้ได้ทั้งแบบอ่านอย่างเดียวและแบบแก้สถานะได้ - อธิบายข้อจำกัดของ Mock State ก่อนเชื่อม Database ได้
ไฟล์ที่แก้
components/IssueBoard.tsx
components/IssueList.tsxโครงสร้างเวลา 60 นาที
| เวลา | เนื้อหา |
|---|---|
| 0-10 นาที | ทบทวน CRUD และ State |
| 10-25 นาที | Update State แบบ Immutable |
| 25-45 นาที | ส่ง Handler เข้า IssueList และสร้างปุ่ม |
| 45-55 นาที | ทดสอบทุก Route และทุก Status |
| 55-60 นาที | สรุป Day 3 และเตรียมเข้า Database |
Slide 1: Recap จาก Hour 3
ตอนนี้หน้า Home ทำงานตาม Flow นี้:
IssueForm Submit
↓
อ่านและตรวจข้อมูล
↓
IssueBoard เพิ่ม Issue เข้า State
↓
IssueList แสดงรายการใหม่สิ่งที่ทำได้แล้ว:
| งาน | สถานะ |
|---|---|
| Create | เพิ่ม Issue เข้า Mock State ได้ |
| Read | แสดงรายการจาก State ได้ |
| Update | ยังเปลี่ยน Status ไม่ได้ |
Hour นี้เราจะเพิ่ม Update โดยยังเก็บข้อมูลไว้ใน Browser เหมือนเดิม
Slide 2: ระบบนี้ไม่จำเป็นต้องลบ Issue
CRUD ย่อมาจาก:
| ตัวอักษร | ความหมาย | ตัวอย่างในระบบ |
|---|---|---|
| C — Create | สร้างข้อมูล | แจ้งปัญหาใหม่ |
| R — Read | อ่านข้อมูล | ดูรายการและรายละเอียด |
| U — Update | แก้ข้อมูล | เปลี่ยน Status |
| D — Delete | ลบข้อมูล | ไม่ใช้เป็น Flow หลักของระบบนี้ |
เมื่อแก้ปัญหาเสร็จ เราจะเปลี่ยน Status เป็น DONE แทนการลบ Issue:
OPEN → IN_PROGRESS → DONEการเก็บรายการเดิมไว้ทำให้ยังย้อนดูประวัติได้ จึงไม่ต้องสร้างปุ่ม “ลบ” หรือ “ปิดงาน” แยก เพราะ DONE สื่อว่าปิดงานแล้ว
Slide 3: ห้ามแก้ State เดิมโดยตรง
ตัวอย่างที่ไม่ควรทำ:
issue.status = "DONE";
setItems(items);โค้ดนี้แก้ Object และ Array เดิม React อาจตรวจไม่พบว่าข้อมูลเปลี่ยน ทำให้ UI ไม่ Render ตามที่คาดไว้
วิธีที่ควรใช้คือสร้างข้อมูลชุดใหม่:
Array เดิม
↓ .map()
Array ใหม่
↓ ...issue
Object ใหม่เฉพาะรายการที่ต้องแก้แนวทางนี้เรียกว่า Immutable Update คือไม่แก้ค่าต้นฉบับ แต่สร้างค่าชุดใหม่จากข้อมูลเดิม
Slide 4: เพิ่ม Handler สำหรับเปลี่ยน Status
แก้ components/IssueBoard.tsx
เพิ่ม IssueStatus ใน Type Import เดิม:
import type { Issue, IssueStatus } from "@/types/issue";เพิ่ม Function นี้ต่อจาก handleCreateIssue() และก่อน return:
function handleUpdateStatus(id: string, status: IssueStatus) {
setItems((currentItems) =>
currentItems.map((issue) =>
issue.id === id
? {
...issue,
status,
}
: issue
)
);
}Function รับข้อมูลสองค่า:
idบอกว่าต้องแก้ Issue รายการไหนstatusคือ Status ใหม่ที่ต้องการ
อ่านการทำงานจากด้านนอกเข้าไปด้านใน:
setItems(...)ใช้เปลี่ยน State ของรายการปัญหาcurrentItemsคือ Array ล่าสุดก่อนแก้ไข.map()วนอ่านissueทีละรายการและสร้าง Array ใหม่issue.id === idตรวจว่าเป็นรายการที่ผู้ใช้ต้องการเปลี่ยนหรือไม่- ถ้า ID ตรงกัน จะ Copy ข้อมูลเดิมด้วย
...issueแล้วแทนค่าstatus - ถ้า ID ไม่ตรงกัน จะคืน
issueเดิมโดยไม่แก้ไข
ผลลัพธ์คือได้ Array ใหม่ที่เปลี่ยน Status เพียงรายการเดียว แล้ว React Render IssueList ใหม่ตาม State ล่าสุด
Slide 5: IssueList ถูกใช้สองรูปแบบ
ก่อนส่ง handleUpdateStatus เข้า IssueList ต้องย้อนดูว่า Component นี้ถูกเรียกใช้ที่ไหนบ้าง:
หน้า Home
└─ IssueBoard เรียก IssueList → ต้องการปุ่มเปลี่ยน Status
หน้า /issues
└─ page.tsx เรียก IssueList → แสดงรายการแบบอ่านอย่างเดียวถ้าบังคับให้ทุก IssueList ต้องได้รับ onUpdateStatus หน้า /issues ซึ่งใช้โค้ดนี้จะเกิด TypeScript Error:
<IssueList issues={issues} />แต่หน้า /issues ไม่ต้องมีปุ่มเปลี่ยน Status จึงไม่ควรสร้าง Handler ที่ไม่ได้ใช้ วิธีแก้คือกำหนด onUpdateStatus เป็น Optional Prop ใน components/IssueList.tsx:
import type { Issue, IssueStatus } from "@/types/issue";
type IssueListProps = {
issues: Issue[];
onUpdateStatus?: (id: string, status: IssueStatus) => void;
};เครื่องหมาย ? หมายถึง Parent จะส่ง Function นี้มาหรือไม่ก็ได้
ส่ง onUpdateStatus มา → แสดงปุ่มและเปลี่ยน Status ได้
ไม่ส่ง onUpdateStatus มา → แสดงรายการแบบอ่านอย่างเดียวSlide 6: ให้ IssueList รับ Handler
เมื่อ IssueList กำลังจะมี onClick ให้เพิ่ม "use client" เป็นบรรทัดแรกของไฟล์ components/IssueList.tsx ก่อน Import:
"use client";จากนั้นแก้ Parameter ของ Function IssueList:
export function IssueList({
issues,
onUpdateStatus,
}: IssueListProps) {จากนั้นเพิ่มรายการ Status ไว้เหนือ Function IssueList:
const statuses: IssueStatus[] = ["OPEN", "IN_PROGRESS", "DONE"];Array นี้ใช้สร้างปุ่มสามปุ่มด้วย .map() แทนการเขียนปุ่มซ้ำทีละสถานะ
IssueList ต้องเป็น Client Component เพราะปุ่มใน Slide ถัดไปจะใช้ Event Handler onClick ส่วนหน้า /issues ยัง Import Client Component นี้จาก Server Component ได้ตามปกติ
Slide 7: เพิ่ม Column จัดการเมื่อมี Handler
แก้ <thead> ใน components/IssueList.tsx
เพิ่มหัว Column ต่อจาก “รายละเอียด”:
<th className="px-3 py-3 font-semibold">รายละเอียด</th>
{onUpdateStatus && (
<th className="px-3 py-3 font-semibold">จัดการ</th>
)}onUpdateStatus && (...) หมายถึง:
- ถ้ามี Handler ให้แสดง Column “จัดการ”
- ถ้าไม่มี Handler ไม่ต้องสร้าง Column นี้
หน้า /issues จึงยังเป็นตารางอ่านอย่างเดียว ส่วนหน้า Home จะแสดงปุ่มเปลี่ยน Status
Slide 8: สร้างปุ่มเปลี่ยน Status
ภายใน <tbody> ให้เพิ่มโค้ดนี้ต่อจาก <td> ที่มี Link “ดูรายละเอียด”:
{onUpdateStatus && (
<td className="px-3 py-4">
<div className="flex flex-wrap gap-2">
{statuses.map((status) => (
<button
key={status}
type="button"
disabled={issue.status === status}
onClick={() => onUpdateStatus(issue.id, status)}
className="rounded-md border border-slate-300 px-2 py-1 text-xs font-semibold text-slate-700 hover:bg-slate-50 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:text-slate-400"
>
{status}
</button>
))}
</div>
</td>
)}.map()สร้างปุ่มจากstatusesonClickส่ง ID และ Status ใหม่กลับไปที่IssueBoarddisabledปิดปุ่มของ Status ปัจจุบัน เพราะกดแล้วข้อมูลจะไม่เปลี่ยนtype="button"ป้องกันปุ่มทำหน้าที่ Submit หาก Component ถูกวางอยู่ใกล้ Form
เมื่อเพิ่ม Column “จัดการ” แล้ว ให้เพิ่มพื้นที่ขั้นต่ำของตารางจาก min-w-[720px] เป็น min-w-[920px]:
<table className="w-full min-w-[920px] border-collapse text-sm">Wrapper เดิมมี overflow-x-auto อยู่แล้ว บนจอเล็กจึงเลื่อนเฉพาะตารางโดยไม่ดันทั้งหน้าให้ล้น
Slide 9: ส่ง Handler จาก IssueBoard
กลับไปที่ components/IssueBoard.tsx แล้วแทน <IssueList> เดิมด้วย:
<IssueList
issues={items}
onUpdateStatus={handleUpdateStatus}
/>Flow ตอนกดปุ่มจะเป็น:
ผู้ใช้กด Status ใน IssueList
↓
onUpdateStatus(issue.id, status)
↓
IssueBoard เรียก handleUpdateStatus()
↓
setItems() สร้าง Array ใหม่
↓
IssueList Render ด้วย items ชุดใหม่IssueBoard เป็นเจ้าของ State จึงเป็น Component ที่มีสิทธิ์เปลี่ยน items ส่วน IssueList เพียงแจ้งว่าผู้ใช้ต้องการทำอะไร
Slide 10: ทดสอบ Update และ Component Reuse
ทดสอบหน้า Home /
- กด
IN_PROGRESSของ Issue ที่เป็นOPEN - ตรวจว่า Badge เปลี่ยนเป็นสีเหลือง
- กด
DONEแล้วตรวจว่า Badge เปลี่ยนเป็นสีเขียว - ตรวจว่าปุ่มของ Status ปัจจุบันกดไม่ได้
- เพิ่ม Issue ใหม่จาก Form แล้วลองเปลี่ยน Status
ทดสอบหน้า /issues
- ตารางยังแสดงรายการครบ
- ไม่มี Column “จัดการ”
- Link “ดูรายละเอียด” ยังทำงาน
- ไม่มี TypeScript Error จากการไม่ส่ง
onUpdateStatus
ทดสอบจอเล็ก
ตรวจว่าตารางเลื่อนภายใน Wrapper และหน้าเว็บไม่มี Scrollbar แนวนอนทั้งหน้า
Slide 11: ข้อจำกัดของ Mock State
ตอนนี้ Create และ Update เกิดใน State ของ IssueBoard:
IssueForm / ปุ่ม Status
↓
useState
↓
IssueListข้อจำกัด:
- Refresh แล้วข้อมูลกลับเป็น Mock Data เดิม
- หน้า Home กับ
/issuesไม่ได้ใช้ State ชุดเดียวกัน - หน้า Detail ยังอ่านข้อมูลจาก
data/issue.ts - Browser เครื่องอื่นไม่เห็นข้อมูลที่เราเพิ่มหรือแก้
- Client Code ไม่ควรเป็นผู้ตรวจสิทธิ์สุดท้าย
State เหมาะสำหรับเรียนรู้การไหลของข้อมูลและการโต้ตอบ แต่ยังไม่ใช่แหล่งเก็บข้อมูลถาวร
Slide 12: Day 4 จะเปลี่ยนอะไร
Day 3:
Component → useState → UIDay 4:
Component → Server → Database → UIสิ่งที่จะเปลี่ยน:
- Database เป็นแหล่งข้อมูลหลักแทน
useState - Refresh แล้วข้อมูลยังอยู่
- ทุก Route อ่านข้อมูลชุดเดียวกัน
- Server ตรวจข้อมูลก่อนบันทึก
- การเปลี่ยน Status ต้องบันทึกลง Database
Hour นี้ยังไม่ต้องสร้าง Server Action หรือ Database Schema เพราะเราจะเริ่มจากการสร้าง Supabase Project และ Table อย่างเป็นขั้นตอนใน Day 4
Slide 13: สรุป Day 3
สิ่งที่ทำได้ใน Day 3:
- เปลี่ยน UI จาก Custom CSS มาใช้ Tailwind
- ทำให้ Form และ Table Responsive
- แยก Server Component และ Client Component
- อ่าน Form ด้วย
FormData - ตรวจข้อมูลและแสดง Error
- Create Issue ใน Mock State
- Update Status แบบ Immutable
- ใช้ Optional Props เพื่อให้ Component เดิมรองรับหลายหน้า
ตอนนี้โปรเจกต์เป็น Frontend Prototype ที่โต้ตอบได้แล้ว แต่ข้อมูลยังอยู่ใน Browser เท่านั้น
ขั้นต่อไปคือเปลี่ยนจาก Mock Data ไปใช้ Supabase เป็นแหล่งข้อมูลจริง
คำศัพท์สำคัญ
| คำศัพท์ | ความหมาย |
|---|---|
| CRUD | Create, Read, Update และ Delete |
| Immutable Update | สร้างข้อมูลชุดใหม่แทนการแก้ Array หรือ Object เดิม |
| Optional Prop | Props ที่ Parent จะส่งมาหรือไม่ก็ได้ |
| Source of Truth | แหล่งข้อมูลหลักที่ระบบเชื่อถือ |
| Mock State | ข้อมูลจำลองที่เก็บชั่วคราวใน Browser |