useState — State คืออะไร และทำไมต้อง Re-render
State คือข้อมูลภายใน Component ที่เมื่อเปลี่ยนแปลงแล้ว React จะ Re-render Component นั้นโดยอัตโนมัติ ต่างจาก Variable ธรรมดาที่เปลี่ยนค่าแล้ว UI ไม่อัปเดต
useState คือ Hook พื้นฐานสำหรับจัดการ State ใน Function Component:
-
ประกาศด้วย
const [value, setValue] = useState(initialValue)—valueคือค่าปัจจุบัน,setValueคือฟังก์ชันสำหรับอัปเดต -
เรียก
setValue(newValue)จะทำให้ React กำหนดค่าใหม่ให้ State แล้ว Re-render Component นั้นและ Children ทั้งหมด -
ห้ามแก้ไข State โดยตรง: ต้องใช้ Setter เสมอ เช่น
setCount(count + 1)ไม่ใช่count = count + 1 -
เมื่อ State ใหม่เป็น Object หรือ Array ต้องสร้างของใหม่:
setForm(prev => ({ ...prev, name: 'ใหม่' }))
React เปรียบ State ใหม่กับเก่าด้วย Object.is() (Shallow Comparison)
หากแก้ไข Object โดยตรง (obj.name = 'ใหม่') React จะคิดว่าไม่มีการเปลี่ยนแปลง
เพราะ Reference ยังเหมือนเดิม จึงต้องสร้าง Object ใหม่ด้วย Spread Operator เสมอ
useEffect — Side Effects และ Dependency Array
Side Effect คือการทำงานที่ไม่ใช่การ Render UI โดยตรง เช่น การดึงข้อมูลจาก API, การตั้ง Timer, การ Subscribe Events, หรือการแก้ไข DOM โดยตรง
useEffect รับ 2 Arguments: ฟังก์ชัน Effect และ Dependency Array:
-
useEffect(() => { ... })— รันทุกครั้งที่ Render (ไม่แนะนำ ส่วนใหญ่ทำให้เกิด Loop) -
useEffect(() => { ... }, [])— รันแค่ครั้งเดียวตอน Component Mount (เหมาะสำหรับดึงข้อมูลครั้งแรก) -
useEffect(() => { ... }, [id])— รันเมื่อidเปลี่ยน (เหมาะสำหรับดึงข้อมูลตาม ID) -
Cleanup Function: ถ้า Effect สร้าง Subscription หรือ Timer ให้
returnฟังก์ชัน Cleanup:return () => clearInterval(timer)
Lifecycle ของ Component กับ useEffect:
Component Mount
│
▼
Render (JSX → DOM)
│
▼
useEffect รัน ◄──── Deps เปลี่ยน ────┐
│ │
▼ │
(รอ...) Re-render
│ ▲
▼ │
Component Unmount setState() เรียก
│
▼
Cleanup Function รัน (ถ้ามี)
Controlled Form ใน React
Controlled Component คือ Form Input ที่ค่าถูกควบคุมโดย React State ทุกการเปลี่ยนแปลงใน Input จะอัปเดต State ผ่าน Event Handler และ React จะ Render Input ด้วยค่าใหม่จาก State (Single Source of Truth)
-
onChange: Event Handler ที่รับ
e.target.valueแล้วเรียก SettersetValue(e.target.value) -
value: ผูก Input กับ State ด้วย
value={formState.field} -
onSubmit + preventDefault: ใช้
e.preventDefault()ป้องกัน Browser Default Behavior (รีโหลดหน้า) แล้วจัดการข้อมูลใน JavaScript -
Dynamic Key Technique: ใช้
[e.target.name]: e.target.valueเพื่อ Update หลาย Fields ด้วย Handler ฟังก์ชันเดียว
Uncontrolled ใช้ ref อ่านค่าตอน Submit (DOM เก็บค่าเอง)
Controlled ใช้ State เก็บทุก Keystroke — แนะนำสำหรับ Validation แบบ Real-time,
ปิด/เปิดปุ่ม Submit ตามเงื่อนไข, หรือต้องการ Sync ค่ากับ UI อื่น
TanStack Router — ทำไมใช้แทน React Router
TanStack Router (เดิมชื่อ React Router v6 แบบ File-based ของ TanStack) เป็น Routing Library รุ่นใหม่ที่ออกแบบมาสำหรับ TypeScript-first และ Type-safe Routes ในคอร์สนี้เราใช้ JavaScript แต่ยังคงได้ประโยชน์จาก API ที่ชัดเจนและ Predictable
เปรียบเทียบกับ React Router v6:
-
Type-safe params: TanStack Router รู้ว่า
$serviceIdเป็น param ทำให้ Auto-complete ทำงานได้ดีกว่า - Explicit Route Tree: กำหนด Route ทั้งหมดในไฟล์เดียว (หรือแยกไฟล์) ทำให้มองเห็น Structure ของแอปได้ชัดเจน
- Loader & Search Params: รองรับ Data Fetching per route และ Validated Search Params ได้ในตัว
- DevTools: มี TanStack Router DevTools ที่ช่วย Debug routes ได้
Link, useParams และ createRoute
TanStack Router มี 3 Concept หลักที่ใช้บ่อย:
-
<Link to="/services">— Component สำหรับ Navigate ระหว่างหน้า ต่างจาก<a href>ตรงที่ไม่รีโหลดหน้า (Client-side Navigation) -
useParams()— Hook สำหรับดึง URL Parameters เช่นconst { serviceId } = useParams({ from: serviceDetailRoute.id })จาก URL/services/123 -
createRoute()— ฟังก์ชันสำหรับสร้าง Route Object กำหนดpath,component, และgetParentRoute -
createRootRoute()— สร้าง Root Route ที่เป็น Parent ของทุก Route โดยปกติ Render<Outlet />เพื่อแสดง Child Route -
createRouter()— รวม Route Tree ทั้งหมดเข้าด้วยกัน แล้วส่งให้<RouterProvider router={router} />ในmain.jsx
TanStack Router ใช้ $ นำหน้า Dynamic Segment เช่น /services/$serviceId
ต่างจาก React Router ที่ใช้ : เช่น /services/:serviceId
ระวังสับสนเมื่ออ่านตัวอย่างจากอินเทอร์เน็ต
// snippet 1: useState + useEffect
import { useState, useEffect } from 'react';
function ServiceList() {
const [services, setServices] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// จำลองการโหลดข้อมูล
setTimeout(() => {
setServices([
{ id: 1, name: 'ตัดผมชาย', price: 150 },
{ id: 2, name: 'นวดแผนไทย', price: 400 },
]);
setLoading(false);
}, 1000);
}, []); // [] = รันแค่ครั้งเดียวตอน mount
if (loading) return <p>กำลังโหลด...</p>;
return (
<ul>
{services.map(s => <li key={s.id}>{s.name} — {s.price} บาท</li>)}
</ul>
);
}
export default ServiceList;
// snippet 2: ตั้งค่า TanStack Router
// src/router.js
import { createRouter, createRoute, createRootRoute } from '@tanstack/react-router';
const rootRoute = createRootRoute();
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: HomePage,
});
const servicesRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/services',
component: ServicesPage,
});
const serviceDetailRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/services/$serviceId',
component: ServiceDetailPage,
});
export const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, servicesRoute, serviceDetailRoute]),
});
// snippet 3: Controlled Form — Booking Form
import { useState } from 'react';
function BookingForm({ serviceId }) {
const [form, setForm] = useState({ date: '', time: '', notes: '' });
function handleChange(e) {
setForm(prev => ({ ...prev, [e.target.name]: e.target.value }));
}
async function handleSubmit(e) {
e.preventDefault();
console.log('ส่งข้อมูลจอง:', { serviceId, ...form });
// จะเชื่อม API จริงใน Week 7
}
return (
<form onSubmit={handleSubmit}>
<input type="date" name="date" value={form.date} onChange={handleChange} required />
<select name="time" value={form.time} onChange={handleChange} required>
<option value="">-- เลือกเวลา --</option>
<option value="09:00">09:00</option>
<option value="10:00">10:00</option>
<option value="11:00">11:00</option>
</select>
<textarea name="notes" value={form.notes} onChange={handleChange} placeholder="หมายเหตุ" />
<button type="submit">ยืนยันการจอง</button>
</form>
);
}
export default BookingForm;
🧪 ปฏิบัติการ Lab 3 — BookEasy: Routing & Booking Form
เพิ่ม TanStack Router เข้าโปรเจกต์ สร้างหน้าต่าง ๆ ด้วย Pages และสร้าง Booking Form แบบ Controlled
ต่อยอดจาก Lab 2 — เพิ่ม routing และ booking form เข้าไปในโปรเจกต์ bookeasy
ติดตั้ง TanStack Router และตั้งค่า Route Tree ครบทุกเส้นทางที่แอปต้องการ
- รัน
npm install @tanstack/react-routerภายใน folderbookeasy/ - สร้าง
src/router.jsพร้อม Root Route และ Routes สำหรับ:/,/services,/services/$serviceId,/booking - แก้ไข
src/main.jsxให้ importrouterและRouterProviderแล้วแทน<App />ด้วย<RouterProvider router={router} /> - Root Route Component ต้อง Render
<Outlet />เพื่อแสดง Child Route ที่ตรงกับ URL ปัจจุบัน
เปิด Dev Server แล้วไม่มี Console Error เกี่ยวกับ Router เข้า URL / แล้วเห็นหน้า Home
(แม้ยังว่างอยู่ก็ได้) ไม่มี "Cannot find module" ใน terminal
สร้าง RootLayout Component ที่ Import Outlet จาก @tanstack/react-router
แล้วใส่ใน createRootRoute({ component: RootLayout })
ตรวจสอบว่า src/router.js Export router ด้วย Named Export ไม่ใช่ Default Export
สร้าง Page Components ทั้ง 3 หน้าและเชื่อมกับ Router ที่ตั้งค่าไว้
-
สร้าง
src/pages/HomePage.jsx— แสดง Hero Section พร้อมชื่อแอปและ ปุ่ม/Link "ดูบริการทั้งหมด" ที่ใช้<Link to="/services"> -
สร้าง
src/pages/ServicesPage.jsx— แสดงServiceCardจาก Mock Data ที่มีอยู่ แต่ละ Card ต้องมี Link ไปยัง/services/{id}โดยใช้<Link to={`/services/${service.id}`}> -
สร้าง
src/pages/ServiceDetailPage.jsx— ดึงserviceIdด้วยuseParams()แสดงรายละเอียดบริการที่ตรงกัน และ RenderBookingFormพร้อมส่งserviceIdเป็น prop -
สร้าง
src/components/BookingForm.jsx— Controlled Form ที่มี Input: วันที่ (date), เวลา (select), หมายเหตุ (textarea) และปุ่ม "ยืนยันการจอง" -
อัปเดต
src/router.jsให้ Import Page Components ที่สร้างใหม่และผูกกับ Route แต่ละตัว
เข้า / เห็น HomePage, เข้า /services เห็นรายการบริการพร้อม Link,
เข้า /services/1 เห็นรายละเอียดบริการ ID 1 และ BookingForm แสดงอยู่ด้านล่าง
ไม่มี Error ใน Console ทุกหน้า
ใน ServiceDetailPage ใช้ const { serviceId } = route.useParams()
(หรือ useParams() ขึ้นกับ TanStack Router version) แล้วค้นหาข้อมูลจาก Mock Array
ด้วย MOCK_SERVICES.find(s => String(s.id) === serviceId)
เพราะ URL Param จะเป็น String เสมอ
ตรวจสอบว่า Routing ทำงานถูกต้องทุก Scenario รวมถึง Browser History
- คลิก Link "ดูบริการทั้งหมด" จาก HomePage ไปยัง ServicesPage ตรวจว่า URL เปลี่ยนเป็น
/services - คลิก ServiceCard ใด ๆ ไปยัง ServiceDetailPage ตรวจว่า URL เป็น
/services/{id}และแสดงข้อมูลถูกต้อง - กดปุ่ม Back ของเบราว์เซอร์ ต้องกลับไปหน้าก่อนหน้าได้โดยไม่รีโหลด
- ทดสอบ Booking Form: กรอกวันที่, เลือกเวลา, กด "ยืนยันการจอง" แล้วดู Console Log ว่าแสดงข้อมูลครบถ้วน
- ทดสอบ Validation: กดปุ่ม Submit โดยไม่กรอกวันที่ เบราว์เซอร์ต้องแสดง Error ว่า Field นั้น Required
Navigate ระหว่างทุกหน้าได้โดยไม่รีโหลด กด Back/Forward ของเบราว์เซอร์ทำงานได้ถูกต้อง
Console Log แสดงข้อมูล Form เมื่อ Submit เช่น { serviceId: "1", date: "2025-07-01", time: "09:00", notes: "" }
ส่งภาพหน้าจอหน้า ServiceDetailPage พร้อม Booking Form ให้อาจารย์
หากเปิด URL โดยตรง (เช่น /services/1) แล้วได้ 404 ให้แก้ไข vite.config.js
เพิ่ม server: { historyApiFallback: true } เพื่อให้ Vite
Redirect ทุก Route กลับมาที่ index.html