โหมดมืด
บทที่ 1 — First React App: Component, JSX, Props, State, Event
🟢 ระดับ: มือใหม่
บทนี้เราจะ:
- สร้าง project React ด้วย Vite
- ดู structure ของ project + เข้าใจไฟล์แต่ละตัว
- เขียน component แรก
- เข้าใจ JSX ทุกบรรทัด — กฎที่ต่างจาก HTML
- ใช้ props, state, event
- ทำ TODO list เล็ก ๆ
- บทนี้สร้าง project ด้วย TypeScript ตั้งแต่แรก (ใช้ template Vite TS — ทบทวน TypeScript ได้ที่บท 0 §4 ถ้าต้องการ หรือใช้ template
--template react(JS เปล่า) แทนก็ได้)
1. ติดตั้ง Node.js
React ต้องการ Node.js (runtime — ตัวรัน JavaScript นอก browser) และจะใช้ npm (Node Package Manager — เครื่องมือติดตั้ง/จัดการ library)
📖 LTS (Long-Term Support = รุ่นที่ได้รับการดูแลระยะยาว — รุ่นที่แนะนำสำหรับงาน production)
ตรวจก่อน:
bash
node --version
npm --versionถ้าไม่มี:
- Windows:
winget install OpenJS.NodeJS.LTS(winget มีมาให้ใน Windows 11 หรือ Windows 10 ที่อัปเดตแล้ว — ถ้าไม่มี winget ให้ดาวน์โหลด Node.js ตรงจาก https://nodejs.org แทน) - macOS:
brew install node - Linux: ผ่าน nvm
💡
winget,brew,nvm= package manager (ตัวจัดการแพ็กเกจ — เครื่องมือลงโปรแกรมในเครื่องผ่าน command line ทำหน้าที่คล้าย Microsoft Store / App Store)
ต้องการ Node 22+ (LTS ปัจจุบัน — แนะนำ Node 22 LTS หรือ Node 24)
💡 ถ้าทำงานหลาย project ที่ต้องการ Node version ต่างกัน แนะนำใช้ nvm (Linux/macOS) หรือ fnm (cross-platform — ทุก OS) เพื่อจัดการหลาย Node version แทนการ install ตรง ๆ — ค้นหา "fnm fast node manager" หรือ "nvm-windows" ได้เลย
2. สร้าง project ด้วย Vite
💡 Vite (อ่าน "วีท") = เครื่องมือ build + dev server สำหรับ React/Vue/Svelte สมัยใหม่ เร็วกว่า Webpack/CRA รุ่นเก่ามาก (ใช้ esbuild สำหรับ dependency pre-bundling และ Rollup (หรือ Rolldown ถ้าเปิด opt-in) สำหรับ production build — เป็นเครื่องมือ compile/bundle เขียนด้วย Go/Rust (ภาษาโปรแกรมอื่น ๆ ที่เร็วมาก — ไม่ต้องรู้ก็ใช้ Vite ได้) — ภายใน) — เป็นมาตรฐานของ React project ใหม่ปี 2026
📟 เปิด terminal ก่อน — Windows: เปิด PowerShell หรือ Windows Terminal (ค้นจาก Start menu); macOS: เปิด Terminal.app; Linux: terminal ของระบบ ใน terminal คุณพิมพ์คำสั่งทีละบรรทัดแล้วกด Enter เพื่อรัน
คำสั่ง
cd= "change directory" = เปลี่ยน folder ปัจจุบันที่ terminal ทำงานอยู่ (เช่นcd C:\code= ไปอยู่ที่ folderC:\code)
bash
# cd ไป folder ที่อยาก create project (เช่น ~/code/ หรือ C:\code\)
npm create vite@latest myapp -- --template react-tsอธิบายคำสั่ง:
npm create vite@latest= เรียก scriptcreate-viteรุ่นล่าสุดmyapp= ชื่อ folder ที่จะสร้าง--(ขีดสองตัว) = ตัวคั่นพิเศษ — ใส่ตามนี้ได้เลย ไม่ต้องสนใจรายละเอียด--template react-ts= ใช้ template "React + TypeScript"
ถ้ายังไม่ถนัด TS → ใช้
--template react(JS เปล่า) ก่อนก็ได้ — เนื้อหาในบทยังตามได้ ข้าม TS syntax (: type,<T>) ไป
bash
cd myapp
npm install
npm run devต้องเห็น:
text
VITE v7.x.x ready in 234 ms
➜ Local: http://localhost:5173/⚠️ ปี 2026 Vite ใช้ v7+ — output อาจต่างเล็กน้อยจากที่แสดง (ไม่กระทบเนื้อหา) รายละเอียดเรื่อง bundler (เครื่องมือรวมไฟล์ JS หลายไฟล์เป็นไฟล์เดียวให้ browser โหลด — Rollup คือรุ่นเดิม, Rolldown คือรุ่นใหม่ที่ Vite กำลังเปิด opt-in) ข้ามได้สำหรับมือใหม่
เปิด browser → http://localhost:5173
⚠️ อย่าปิด terminal — dev server ต้องรันอยู่ตลอดที่คุณกำลังพัฒนา ถ้าปิด terminal แล้วอยากรัน server ใหม่ให้รัน
npm run devอีกครั้ง
🎉 เห็น React + Vite logo + counter — app run แล้ว
3. ดู project structure
text
myapp/
├── package.json ← dependency + script
├── vite.config.ts ← config Vite
├── tsconfig.json ← config TypeScript
├── index.html ← root HTML (มี <div id="root">)
├── public/ ← static files
├── src/
│ ├── main.tsx ← entry — render App ลงใน <div id="root">
│ ├── App.tsx ← root component
│ ├── App.css ← style ของ App
│ ├── index.css ← style ทั่วไป
│ ├── assets/
│ └── vite-env.d.ts ← Vite type definitions
└── node_modules/ ← dependencies — folder ที่ npm ดาวน์โหลด library ทุกตัวลงมาเก็บ (อาจมีหลายพันไฟล์ — ปกติ อย่าแก้ — และห้าม commit เข้า git (ไฟล์ .gitignore ที่ Vite สร้างให้จะ ignore folder นี้ให้อัตโนมัติ))3.1 ดู index.html
html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>แค่ <div id="root"></div> — React จะใส่ทุกอย่างที่นี่
3.2 ดู main.tsx
tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)วิเคราะห์:
createRoot(...)— สร้าง "ราก" ของ React app ตรง DOM element ที่ระบุ (ที่นี่คือ<div id="root">)getElementById('root')!—!ตอนท้ายเป็น TypeScript non-null assertion (การยืนยันว่าค่านี้ไม่เป็น null — การันตีให้ TS แต่ถ้าจริง ๆ เป็น null จะเกิด runtime error) = บอก TS ว่า "รับรองว่าไม่ null" ในกรณีนี้ปลอดภัย เพราะไฟล์ index.html มี<div id="root">อยู่แน่ ๆ แล้ว (ถ้าใช้ template JS เปล่า ๆ ไม่มี!ตัวนี้) — เจอ syntax ใหม่ตรงนี้ที่ยังไม่เห็นในบท 0 (§4 ยังไม่ครอบคลุม!) อธิบายทันทีตรงนี้เลย.render(<StrictMode><App /></StrictMode>)— สั่ง React วาด component นี้<App />— root component (เริ่มต้นของ tree)<StrictMode>— เปิด check เพิ่ม (เฉพาะ dev — ไม่เห็นใน production)
🤔
<StrictMode>ทำให้ render สองครั้ง — ปกติไหม?ปกติ! ใน dev mode (รัน
npm run dev) StrictMode ตั้งใจเรียก component ซ้ำเพื่อช่วยจับ bug ที่อาจซ่อนอยู่ — เห็นconsole.log('render')ออก 2 ครั้ง = ปกติ ใน production build (npm run build) จะออกครั้งเดียวรายละเอียดเชิงลึก (ข้ามได้ตอนนี้): StrictMode ช่วย detect side effect ใน render, missing cleanup ใน useEffect, deprecated API — รายละเอียดในบท 2 (Hooks) และบท 14 (Internals) เก็บ StrictMode ไว้เสมอ — มันช่วยจับ bug ก่อน user เจอ
3.3 ดู App.tsx
tsx
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg' // path ขึ้นต้นด้วย / = ไฟล์ใน public/ folder (root ของ project)
import './App.css' // ./ = path สัมพัทธ์จากไฟล์ปัจจุบัน
function App() {
const [count, setCount] = useState(0)
return (
<>
<div>
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
{/* ... = โค้ดที่เหลือของ template (React logo + link) — ยังไม่สำคัญตอนนี้ */}
{/* ⚠️ โค้ดบล็อกนี้เป็นตัวอย่างสำหรับอ่านทำความเข้าใจเท่านั้น — อย่าคัดลอกไปแทนไฟล์จริง เพราะตัดโค้ดบางส่วนออก */}
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
</div>
</>
)
}
export default Appอ่าน:
function App() { ... return JSX }— component คือ functionuseState(0)— สร้าง statereturn (...)— return JSXexport default App— export
4. JSX — รายละเอียดที่ทุกคนต้องรู้
JSX (อ่าน "เจ-เอส-เอ็กซ์") = syntax extension ของ JavaScript — เขียน HTML-like ใน JS ได้
jsx
const element = <h1>Hello, World!</h1>;Vite compile (แปลงโค้ดให้กลายเป็น JS ที่ browser อ่านได้ — เกิดอัตโนมัติเมื่อ save ไฟล์) โดยใช้ Babel (เครื่องมือแปลง JSX → JS ที่ browser รู้จัก — ทำงานอัตโนมัติผ่าน Vite) — React 17+ ใช้ new JSX transform: compile เป็น _jsx() จาก react/jsx-runtime (ไม่ต้อง import React from 'react' อีกต่อไป — Vite template ใหม่จะ config ให้แล้ว) ซึ่งเทียบเท่าผล:
js
const element = React.createElement('h1', null, 'Hello, World!');JSX ดูเหมือน HTML แต่จริงเป็น JS — มี กฎ ที่ต่าง
4.1 ใส่ JavaScript ใน { }
jsx
const name = "Anna";
const age = 25;
return (
<div>
<p>Name: {name}</p>
<p>Age: {age * 2}</p>
<p>Status: {age >= 18 ? "Adult" : "Minor"}</p>
</div>
);ทุกอย่างใน {} คือ JS — สามารถใส่ expression (นิพจน์ — โค้ดที่มีค่าคืนออกมา เช่น 1+1, name.toUpperCase() (แปลง name เป็นตัวพิมพ์ใหญ่ทั้งหมด), age >= 18 ? "Adult" : "Minor") ใด ๆ ก็ได้ แต่ใส่ statement (คำสั่ง — เช่น if, for ที่ไม่คืนค่า) ไม่ได้
4.2 ใช้ className ไม่ใช่ class
jsx
<div className="card"> {/* ✅ */}
<div class="card"> {/* ❌ class เป็น keyword JS */}4.3 ใช้ camelCase สำหรับ attribute
jsx
<input type="text" onChange={handler} maxLength={10} />
{/* ไม่ใช่ onchange, maxlength */}4.4 self-close tag
jsx
<img src="..." /> {/* ✅ */}
<input type="text" /> {/* ✅ */}
<br /> {/* ✅ */}
<img src="..."> {/* ❌ ต้อง self-close */}4.5 ต้อง wrap ในตัวเดียว
jsx
// ❌ error — compile ไม่ผ่าน ("Adjacent JSX elements must be wrapped...")
return (
<p>First</p>
<p>Second</p>
);
// ✅ wrap ใน div
return (
<div>
<p>First</p>
<p>Second</p>
</div>
);
// ✅ ใช้ fragment (ไม่เพิ่ม element)
// Fragment = tag ว่างพิเศษ <>...</> ใช้ wrap หลาย element โดยไม่เพิ่ม <div> จริงใน HTML output
return (
<>
<p>First</p>
<p>Second</p>
</>
);4.6 inline style เป็น object
jsx
<div style={{ color: 'red', fontSize: '20px' }}>Hello</div>
{/* ใส่ double {{ }} เพราะ outer = JS expression, inner = object */}camelCase + ค่าเป็น string
4.7 conditional rendering
jsx
{isLoggedIn ? <Dashboard /> : <Login />} // ternary
{isLoggedIn && <Dashboard />} // ถ้า true → render, ถ้า false → ไม่
{count > 0 ? <p>{count} items</p> : null} // ไม่ render = null⚠️ 0 && <X/> จะ render 0 เพราะ 0 เป็น falsy (= ค่าที่ถือว่าเป็น false ใน JS เช่น 0, "", null, undefined, false) แต่ 0 render ได้ — ใช้ count > 0 ? แทน
4.8 render list
jsx
const fruits = ["apple", "banana", "cherry"];
return (
<ul>
{fruits.map(f => <li key={f}>{f}</li>)}
</ul>
);key prop สำคัญมาก — React ใช้ key บอกว่า "item ตัวไหนคือตัวไหน" เวลา list เปลี่ยน (เพิ่ม/ลบ/สลับลำดับ) ถ้าไม่มี key React อาจ render ผิด (เช่น เก็บ state ของ item ผิดตัว) หรือช้า (re-render ทั้งหมดแทนเฉพาะที่เปลี่ยน)
- ใช้ id เสมอ ถ้ามี
- index ใช้ได้ถ้า list เป็น static (ไม่มีการเพิ่ม/ลบ/สลับลำดับ) เช่น tab menu คงที่ — ถ้า list มี form input ห้ามใช้ index เด็ดขาด เพราะอาจ map state ผิดตัวหลัง delete
5. Component — function ที่ return JSX
📖 คำศัพท์ที่จะเจอตั้งแต่ §5 เป็นต้นไป (มือใหม่อ่านก่อน):
- component = function ที่คืน JSX → ใช้สร้าง UI
- props = "พารามิเตอร์" ของ component (ข้อมูลที่ parent ส่งให้ child)
- state = ข้อมูลภายใน component ที่เปลี่ยนได้ + ทำให้ component re-render เมื่อเปลี่ยน
- hook = function ที่ขึ้นต้นด้วย
use(เช่นuseState,useEffect) — เป็น "ช่อง" ที่ component ใช้ผูกกับระบบของ React (รายละเอียดบท 2)- render = React สร้าง DOM ออกหน้าจอจาก component + state ปัจจุบัน
- re-render = render ใหม่ — เกิดทุกครั้งที่ state/props เปลี่ยน
JS/TS syntax ที่จะเจอ (ทบทวนจากบท 0 ถ้าลืม):
{ name, age }ใน argument = destructuring → ดึง field จาก object เป็นตัวแปร: GreetingPropsหลัง argument = TypeScript type annotation (บอกชนิดของ argument)() => ...= arrow function (function แบบสั้น)...objใน{ ...user, age: 26 }= spread (กระจาย field ของ object เดิม)
tsx
function Greeting() {
return <h1>Hello, World!</h1>;
}
// ใช้
<Greeting />กฎ: ชื่อ component ขึ้นต้นด้วยตัวใหญ่ — <greeting /> จะถูกตีความเป็น HTML tag
5.1 Props — ส่งข้อมูลเข้า component
tsx
type GreetingProps = {
name: string;
age: number;
};
function Greeting({ name, age }: GreetingProps) {
return <p>Hello, {name}! You are {age} years old.</p>;
}
// ใช้
<Greeting name="Anna" age={25} />
<Greeting name="Ben" age={30} />name="Anna"— string ใช้ ""age={25}— non-string ใช้{}
5.2 Children prop
tsx
type CardProps = {
title: string;
children: React.ReactNode; // React.ReactNode = type ที่หมายถึง "อะไรก็ได้ที่ React render เป็น UI ได้" (string, number, JSX, array ของ JSX, null, ...)
};
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div>{children}</div>
</div>
);
}
// ใช้
<Card title="User Profile">
<p>Name: Anna</p>
<p>Age: 25</p>
</Card>children = ทุกอย่างที่อยู่ระหว่าง <Card>...</Card>
5.3 Props เป็น read-only
tsx
function Greeting({ name }: { name: string }) {
name = "Hacked"; // ❌ อย่าทำ — props ห้ามแก้
return <p>{name}</p>;
}โค้ดนี้รันได้ ไม่มี error ทันที แต่ผลลัพธ์ไม่ sync กับ parent (parent ไม่รู้ว่าค่าถูกเปลี่ยน) และงงตอน re-render (ค่าอาจเด้งกลับเป็นของเดิมจาก parent) — เป็น anti-pattern ไม่ใช่ syntax error
ถ้าจะเปลี่ยน → ใช้ state
6. State — ข้อมูลที่ component เก็บ + เปลี่ยนได้
tsx
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}6.1 useState — anatomy
tsx
const [count, setCount] = useState(0);
// ^ ^ ^
// | | initial value
// | setter — เรียกเพื่อเปลี่ยน
// getter — อ่านค่าปัจจุบันuseState return array 2 ตัว — destructure เป็น [value, setter]
6.2 ⚠️ ห้ามแก้ state ตรง ๆ
tsx
// ❌ wrong
count = 5;
count++;
// ✅ right
setCount(5);
setCount(count + 1);6.3 State แบบ functional update
tsx
setCount(count + 1); // ใช้ value ปัจจุบัน
setCount(prev => prev + 1); // ใช้ value ล่าสุด (ปลอดภัยกว่า)รายละเอียดทำไม functional update ปลอดภัยกว่า (เกี่ยวกับ closure) → บท 2
ถ้าเรียก setCount หลายครั้งใน event เดียว — ใช้ functional ปลอดภัยกว่า:
tsx
// ❌ count เพิ่มแค่ 1 (ทั้งสามครั้งใช้ count เดิม)
function handle() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}
// ✅ count เพิ่ม 3
function handle() {
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
}6.4 State แบบ object/array — ต้อง immutable
tsx
const [user, setUser] = useState({ name: "Anna", age: 25 });
// ❌ wrong — mutate
user.age = 26;
setUser(user); // React ไม่เห็นว่าเปลี่ยน
// ✅ create new object
setUser({ ...user, age: 26 });
setUser(prev => ({ ...prev, age: prev.age + 1 }));tsx
const [items, setItems] = useState<string[]>([]);
// ❌ wrong
items.push("new");
setItems(items);
// ✅
setItems([...items, "new"]);
setItems(prev => [...prev, "new"]);
// ลบ
setItems(prev => prev.filter(x => x !== "old"));
// แก้
setItems(prev => prev.map(x => x === "old" ? "new" : x));7. Event Handling
tsx
<button onClick={() => alert("clicked")}>Click</button>
<input
onChange={e => setText(e.target.value)}
onFocus={() => console.log("focused")} // เมื่อ user คลิกเข้ามาในช่อง
onBlur={() => console.log("blurred")} // เมื่อ user คลิกออกจากช่อง
/>
<form onSubmit={e => {
e.preventDefault(); // กัน form submit ปกติ
handleSubmit();
}}>Event handler:
- ใช้ camelCase:
onClick,onChange,onSubmit - ค่าเป็น function (ไม่ใช่ string)
- รับ event object เป็น argument
Event types ใน TS
tsx
function Button() {
// React.MouseEvent<HTMLButtonElement> = type ของ event object ที่มาจาก <button> เวลาถูกคลิก
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.currentTarget);
};
return <button onClick={handleClick}>Click</button>;
}8. TODO List — combine ทุกอย่าง
ปิดท้ายบทด้วยตัวอย่างที่รวมทุกอย่างที่เรียนมา — TODO list ที่ใช้ทั้ง state, event handler, การ render list และ conditional rendering ลองอ่านโค้ดนี้แล้วจะเห็นว่าแต่ละชิ้นที่เรียนแยกกันประกอบเป็น app จริงได้อย่างไร:
tsx
import { useState } from 'react';
type Todo = {
id: number;
text: string;
done: boolean;
};
function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([]);
const [input, setInput] = useState("");
const addTodo = () => {
if (input.trim() === "") return;
const newTodo: Todo = {
id: Date.now(), // พอสำหรับตัวอย่างนี้ — production ควรใช้ crypto.randomUUID() หรือ nanoid แทน
text: input,
done: false,
};
setTodos(prev => [...prev, newTodo]);
setInput("");
};
const toggleTodo = (id: number) => {
setTodos(prev => prev.map(t =>
t.id === id ? { ...t, done: !t.done } : t
));
};
const deleteTodo = (id: number) => {
setTodos(prev => prev.filter(t => t.id !== id));
};
return (
<div>
<h1>Todo List</h1>
<div>
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => e.key === "Enter" && addTodo()}
placeholder="สิ่งที่ต้องทำ..."
/>
<button onClick={addTodo}>Add</button>
</div>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{
textDecoration: todo.done ? 'line-through' : 'none'
}}>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>×</button>
</li>
))}
</ul>
<p>Total: {todos.length}, Done: {todos.filter(t => t.done).length}</p>
</div>
);
}
export default TodoApp;แทน App.tsx ด้วยอันนี้ → save → Vite hot reload (โหลดหน้าเว็บใหม่อัตโนมัติเมื่อ save ไฟล์ ไม่ต้อง refresh browser เอง) → ลองเล่น
9. แยก Component
โค้ดบนทำได้ แต่ component ยาว → แตก:
📝 ขั้นตอนก่อนสร้างไฟล์ใหม่:
- สร้าง folder ชื่อ
componentsในsrc/ก่อน (คลิกขวา > New Folder ใน editor หรือรันmkdir src/componentsใน terminal)- สร้างไฟล์ใหม่ใน folder นั้น
tsx
// components/TodoItem.tsx
// นิยาม Todo type ในไฟล์นี้ด้วย หรือ import จาก App.tsx ถ้า export ออกมาแล้ว
type Todo = { id: number; text: string; done: boolean };
type Props = {
todo: Todo;
onToggle: (id: number) => void;
onDelete: (id: number) => void;
};
function TodoItem({ todo, onToggle, onDelete }: Props) {
return (
<li>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
<span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => onDelete(todo.id)}>×</button>
</li>
);
}
export default TodoItem;tsx
// components/TodoInput.tsx
import { useState } from 'react';
type Props = {
onAdd: (text: string) => void;
};
function TodoInput({ onAdd }: Props) {
const [input, setInput] = useState("");
const submit = () => {
if (input.trim() === "") return;
onAdd(input);
setInput("");
};
return (
<div>
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => e.key === "Enter" && submit()}
placeholder="สิ่งที่ต้องทำ..."
/>
<button onClick={submit}>Add</button>
</div>
);
}
export default TodoInput;tsx
// App.tsx
import { useState } from 'react';
import TodoItem from './components/TodoItem';
import TodoInput from './components/TodoInput';
type Todo = { id: number; text: string; done: boolean };
function App() {
const [todos, setTodos] = useState<Todo[]>([]);
const addTodo = (text: string) =>
setTodos(prev => [...prev, { id: Date.now(), text, done: false }]);
const toggleTodo = (id: number) =>
setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t));
const deleteTodo = (id: number) =>
setTodos(prev => prev.filter(t => t.id !== id));
return (
<div>
<h1>Todo List</h1>
<TodoInput onAdd={addTodo} />
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
))}
</ul>
</div>
);
}
export default App;ดูแล้ว:
- State อยู่ที่ App (parent) — เรียกว่า "lift state up" (ยก state ขึ้นไปไว้ที่ parent เพื่อให้ลูก ๆ ใช้ร่วมกันได้ — ถ้าอยู่ที่ child แต่ละตัว, share ข้อมูลกันไม่ได้)
- ส่ง data ลง child ผ่าน props
- ส่ง action ขึ้น parent ผ่าน callback props
10. กฎ React ที่ต้องจำ
10.1 ขึ้นต้นด้วยตัวใหญ่เสมอ
tsx
function MyComponent() { ... } // ✅
function myComponent() { ... } // ❌ React คิดว่าเป็น HTML tag10.2 Return JSX 1 อันเดียว
ใช้ fragment <></> ถ้าไม่อยาก wrap div
10.3 ใช้ key prop ใน list
jsx
items.map(i => <Item key={i.id} ... />)10.4 ห้าม mutate state
ใช้ spread (...) สร้าง array/object ใหม่ → setState
10.5 ห้ามเรียก hook ใน condition/loop
tsx
// ❌ wrong — เรียก hook ใน if
function Bad() {
if (some) {
const [x, setX] = useState(0); // ❌
}
}
// ✅ right — เรียก hook ที่ top level ของ component (ใน body ของ function โดยตรง)
function Good() {
const [x, setX] = useState(0); // ✅
if (some) {
// ใช้ x / setX ตรงนี้ได้
}
}top level = ใน body ของ component โดยตรง (ไม่ใช่ใน if / for / try / function ซ้อน) — hook ต้องเรียกที่ top level เสมอ (เดี๋ยวบทถัดไปอธิบายทำไม)
10.6 ห้ามแก้ DOM ตรง ๆ
ใช้ React state แทน — ปล่อย React จัดการ DOM
11. CSS — มีหลายวิธี
11.1 External CSS file
tsx
import './App.css';
<div className="card">...</div>11.2 Inline
tsx
<div style={{ color: 'red', padding: 10 }}>...</div>11.3 CSS Modules
tsx
import styles from './App.module.css';
<div className={styles.card}>...</div>11.4 Tailwind CSS (popular ปี 2024-2026)
tsx
<div className="bg-blue-500 text-white p-4 rounded">...</div>— class utility ที่ใช้ทันที ไม่ต้องเขียน CSS file
⚠️ ต้องลง Tailwind ก่อน (วิธีติดตั้งดูบท 6) — ใส่ class พวกนี้ใน plain CSS template ของ Vite (ที่ยังไม่ได้ลง Tailwind) จะไม่มีผล
11.5 styled-components / Emotion (CSS-in-JS — เขียน CSS อยู่ในไฟล์ JS โดยตรง)
ใช้น้อยลงในปี 2026 — Tailwind ครองตลาด
12. Checkpoint
🛠️ Checkpoint 1.1 — ปรับ Todo
เพิ่ม feature:
- ปุ่ม "Clear completed" ลบเฉพาะ todo ที่ done
- Filter: All / Active / Done
💡 ส่วน "จำ todos ใน localStorage (เปิด/ปิด browser แล้วยังอยู่)" ต้องใช้
useEffectซึ่งจะสอนในบท 2 — เก็บไว้ทำต่อหลังเรียนบท 2 (จะมี custom hookuseLocalStorageในบท 2 §9)
📖 เฉลย (key parts)
tsx
const [filter, setFilter] = useState<'all' | 'active' | 'done'>('all');
const filtered = todos.filter(t => {
if (filter === 'active') return !t.done;
if (filter === 'done') return t.done;
return true;
});
const clearCompleted = () => setTodos(prev => prev.filter(t => !t.done));🛠️ Checkpoint 1.2 — Calculator
สร้าง simple calculator:
- input 2 ตัว (number)
- เลือก operation (+, -, *, /)
- แสดงผล
📖 เฉลย
tsx
function Calculator() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [op, setOp] = useState<'+' | '-' | '*' | '/'>('+');
const result = (() => {
switch (op) {
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '/': return b !== 0 ? a / b : 'error';
// ไม่ต้องมี default — TypeScript รู้ว่า op เป็น union type '+' | '-' | '*' | '/' เท่านั้น
// เช็คครบ 4 case แล้วจึง exhaustive (ครอบคลุมทุกความเป็นไปได้) แล้ว
}
})();
return (
<div>
<input type="number" value={a} onChange={e => setA(Number(e.target.value))} />
<select value={op} onChange={e => setOp(e.target.value as '+' | '-' | '*' | '/')}>
<option>+</option><option>-</option><option>*</option><option>/</option>
</select>
<input type="number" value={b} onChange={e => setB(Number(e.target.value))} />
<span> = {result}</span>
</div>
);
}13. สรุปบท
✅ Vite + React + TypeScript = stack เริ่มต้น ✅ Component = function ที่ return JSX (ตัวใหญ่ขึ้นต้น) ✅ JSX: className, camelCase attribute, { } ใส่ JS, fragment <></> ✅ Props: ส่ง data จาก parent → child (read-only) ✅ State: const [v, setV] = useState(initial) — เปลี่ยนผ่าน setter เท่านั้น ✅ Immutable updates: spread [...prev, x], {...prev, key: v} ✅ Event: onClick, onChange — function ไม่ใช่ string ✅ List + key prop ✅ Lift state up — state อยู่ที่ parent, ส่ง callback ลง child
14. ต่อจากนี้
บทที่จะตามมา (ดูสารบัญเต็มได้ที่ README):
| หัวข้อ | สอนอะไร |
|---|---|
| 02 — Hooks ลึก | useEffect, useMemo, useCallback, useRef, useContext, custom hooks |
| 03 — Form + Validation | controlled component, React Hook Form, validation |
| 04 — Data fetching | fetch, useEffect pattern, TanStack Query |
| 05 — Routing | React Router — multi-page SPA |
| 06 — Styling + UI Libraries | Tailwind CSS, CSS Modules, shadcn/ui |
| 07 — State management | Zustand, Redux Toolkit, Context |
| 08 — Testing | Vitest, React Testing Library |
ตอนนี้ฝึก checkpoint บทนี้ให้คล่อง — เป็นพื้นฐานทุกบทต่อ
← กลับสารบัญ React Book | ← กลับสารบัญหลัก | → บทที่ 2: Hooks เชิงลึก