FileEditViewTerminal
IT Issue Bootcamp - Visual Studio Code
day-3-hour-4.mdx

Day 3 / Hour 4

Mock CRUD and Database Prep

Mock update, close concept, filters, database transition, and Server Actions preview.

60 minutes
Learners understand the CRUD flow before Supabase.

Mock CRUD and Database Prep

Day 3 - ชั่วโมงที่ 4: เปลี่ยนสถานะใน Mock State

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

หลังจบชั่วโมงนี้ นักศึกษาจะสามารถ:

  1. อธิบาย Create, Read และ Update ในโปรเจกต์ปัจจุบันได้
  2. เปลี่ยน Status ของ Issue ใน State ได้
  3. Update Array และ Object แบบไม่แก้ค่าเดิมโดยตรง
  4. ส่ง Event Handler จาก Parent ไปยัง Child ผ่าน Props
  5. ทำให้ IssueList ใช้ได้ทั้งแบบอ่านอย่างเดียวและแบบแก้สถานะได้
  6. อธิบายข้อจำกัดของ 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 ใหม่ที่ต้องการ

อ่านการทำงานจากด้านนอกเข้าไปด้านใน:

  1. setItems(...) ใช้เปลี่ยน State ของรายการปัญหา
  2. currentItems คือ Array ล่าสุดก่อนแก้ไข
  3. .map() วนอ่าน issue ทีละรายการและสร้าง Array ใหม่
  4. issue.id === id ตรวจว่าเป็นรายการที่ผู้ใช้ต้องการเปลี่ยนหรือไม่
  5. ถ้า ID ตรงกัน จะ Copy ข้อมูลเดิมด้วย ...issue แล้วแทนค่า status
  6. ถ้า 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() สร้างปุ่มจาก statuses
  • onClick ส่ง ID และ Status ใหม่กลับไปที่ IssueBoard
  • disabled ปิดปุ่มของ 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 /

  1. กด IN_PROGRESS ของ Issue ที่เป็น OPEN
  2. ตรวจว่า Badge เปลี่ยนเป็นสีเหลือง
  3. กด DONE แล้วตรวจว่า Badge เปลี่ยนเป็นสีเขียว
  4. ตรวจว่าปุ่มของ Status ปัจจุบันกดไม่ได้
  5. เพิ่ม Issue ใหม่จาก Form แล้วลองเปลี่ยน Status

ทดสอบหน้า /issues

  1. ตารางยังแสดงรายการครบ
  2. ไม่มี Column “จัดการ”
  3. Link “ดูรายละเอียด” ยังทำงาน
  4. ไม่มี 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 → UI

Day 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 เป็นแหล่งข้อมูลจริง


คำศัพท์สำคัญ

คำศัพท์ความหมาย
CRUDCreate, Read, Update และ Delete
Immutable Updateสร้างข้อมูลชุดใหม่แทนการแก้ Array หรือ Object เดิม
Optional PropProps ที่ Parent จะส่งมาหรือไม่ก็ได้
Source of Truthแหล่งข้อมูลหลักที่ระบบเชื่อถือ
Mock Stateข้อมูลจำลองที่เก็บชั่วคราวใน Browser