Skip to content

Study Guide: React + 3 Essential Hooks

App.jsx LIVE DEMO
App
Header
Main
Card
Card
Card
Footer
Header
Design color="pink"
Code color="blue"
Ship color="green"
<App />

คลิกที่ node ในต้นไม้เพื่อ highlight component ที่ render ออกมา

0
const [count, setCount] = useState(0)
const [color, setColor] = useState("#3b82f6")

สังเกต flash effect ตอนกด -- นั่นคือ re-render! React render component ใหม่เมื่อ state เปลี่ยน

0s
useEffect(() => { setInterval(...) }, [])
Saved!
useEffect(() => { localStorage.setItem(...) }, [text])
// Console output
useEffect(() => { console.log("mounted"); return () => console.log("cleanup") }, [])
[] ทำครั้งเดียวตอน mount
[value] ทำเมื่อ value เปลี่ยน
ไม่ใส่ ทำทุกรอบ render
const filtered = useMemo(() => items.filter(...), [query, category])
Filter calculations
0 with useMemo
0 without useMemo

ลอง toggle ปิด useMemo แล้วพิมพ์ -- สังเกตว่า "without" นับขึ้นทุก keystroke, "with" นับเฉพาะเมื่อ filter เปลี่ยนจริง

React คือ JavaScript library สำหรับสร้าง UI — แทนที่จะจัดการ DOM เองทีละตัว React ให้เราเขียน Component ที่อธิบายว่า “UI ควรหน้าตาเป็นยังไง” แล้ว React จัดการ DOM ให้

Component = Function ที่ return HTML

Section titled “Component = Function ที่ return HTML”
// ทุก Component คือ function ที่ return JSX (HTML-like syntax)
function Greeting() {
return <h1>สวัสดี!</h1>;
}
// ใช้ component เหมือน HTML tag
function App() {
return (
<div>
<Greeting />
<Greeting />
</div>
);
}
function ProfileCard() {
const name = "Alex";
const isOnline = true;
return (
<div className="card">
{/* ใส่ค่า JS ด้วย {} */}
<h2>{name}</h2>
<p>Status: {isOnline ? "Online" : "Offline"}</p>
{/* className แทน class */}
<button className="btn">Follow</button>
{/* style ใช้ object */}
<span style={{ color: "green", fontSize: "14px" }}>Active</span>
</div>
);
}

Props — ส่งข้อมูลให้ Component

Section titled “Props — ส่งข้อมูลให้ Component”
// รับ props เป็น parameter
function UserCard({ name, role, avatarUrl }) {
return (
<div className="flex items-center gap-3 p-4 rounded-xl bg-white shadow">
<img src={avatarUrl} alt={name} className="w-12 h-12 rounded-full" />
<div>
<h3 className="font-bold">{name}</h3>
<p className="text-sm text-gray-500">{role}</p>
</div>
</div>
);
}
// ใช้งาน — ส่ง props เหมือน HTML attributes
function TeamPage() {
return (
<div className="space-y-3">
<UserCard name="Alex" role="Designer" avatarUrl="/alex.jpg" />
<UserCard name="Ploy" role="Developer" avatarUrl="/ploy.jpg" />
<UserCard name="Bank" role="PM" avatarUrl="/bank.jpg" />
</div>
);
}
function TodoList({ todos }) {
return (
<ul className="space-y-2">
{todos.map((todo) => (
<li key={todo.id} className="flex items-center gap-2">
<input type="checkbox" checked={todo.completed} />
<span>{todo.text}</span>
</li>
))}
</ul>
);
}
Output
Component Tree
App
|-- Header
|-- Card title="Design"
|-- Card title="Code"
Rendered Output
My App
🎨
Design
💻
Code
<App />
Click a node to highlight its rendered output

Hook #1: useState — จัดการ State

Section titled “Hook #1: useState — จัดการ State”

useState คือ function ที่ให้เรา สร้างตัวแปรที่พอเปลี่ยนค่า React จะ render ใหม่อัตโนมัติ

import { useState } from "react";
function Counter() {
// [ค่าปัจจุบัน, function สำหรับเปลี่ยนค่า] = useState(ค่าเริ่มต้น)
const [count, setCount] = useState(0);
return (
<div className="text-center p-8">
<p className="text-4xl font-bold mb-4">{count}</p>
<div className="flex gap-2 justify-center">
<button onClick={() => setCount(count - 1)} className="btn">-</button>
<button onClick={() => setCount(0)} className="btn">Reset</button>
<button onClick={() => setCount(count + 1)} className="btn">+</button>
</div>
</div>
);
}
function TodoApp() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState("");
const addTodo = () => {
if (!input.trim()) return;
// สร้าง array ใหม่ (ห้าม mutate ตัวเดิม!)
setTodos([...todos, { id: Date.now(), text: input, completed: false }]);
setInput("");
};
const toggleTodo = (id) => {
setTodos(todos.map((t) =>
t.id === id ? { ...t, completed: !t.completed } : t
));
};
const deleteTodo = (id) => {
setTodos(todos.filter((t) => t.id !== id));
};
return (
<div>
<div className="flex gap-2 mb-4">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && addTodo()}
placeholder="New todo..."
className="flex-1 px-3 py-2 border rounded-lg"
/>
<button onClick={addTodo} className="px-4 py-2 bg-blue-600 text-white rounded-lg">
Add
</button>
</div>
<ul className="space-y-2">
{todos.map((todo) => (
<li key={todo.id} className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span className={todo.completed ? "line-through text-gray-400" : ""}>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)} className="ml-auto text-red-500">
Delete
</button>
</li>
))}
</ul>
</div>
);
}
Output
0
0
const [count, setCount] = useState(0)

Hook #2: useEffect — จัดการ Side Effects

Section titled “Hook #2: useEffect — จัดการ Side Effects”

useEffect ใช้สำหรับสิ่งที่ ไม่ใช่การ render — เช่น fetch data, localStorage, timer, event listener

import { useState, useEffect } from "react";
function App() {
const [todos, setTodos] = useState([]);
// อ่านจาก localStorage ตอน component โหลดครั้งแรก
useEffect(() => {
const saved = localStorage.getItem("todos");
if (saved) {
setTodos(JSON.parse(saved));
}
}, []); // [] = ทำครั้งเดียวตอน mount
// บันทึกลง localStorage ทุกครั้งที่ todos เปลี่ยน
useEffect(() => {
localStorage.setItem("todos", JSON.stringify(todos));
}, [todos]); // [todos] = ทำเมื่อ todos เปลี่ยน
return <div>...</div>;
}
// ไม่ใส่ [] = ทำทุกครั้งที่ render (ระวัง infinite loop!)
useEffect(() => { /* ... */ });
// ใส่ [] = ทำครั้งเดียวตอน mount
useEffect(() => { /* ... */ }, []);
// ใส่ [value] = ทำเมื่อ value เปลี่ยน
useEffect(() => { /* ... */ }, [count, name]);

ตัวอย่าง: Dark Mode กับ useEffect

Section titled “ตัวอย่าง: Dark Mode กับ useEffect”
function ThemeToggle() {
const [isDark, setIsDark] = useState(false);
// อ่าน theme จาก localStorage ตอนเริ่มต้น
useEffect(() => {
const saved = localStorage.getItem("theme");
setIsDark(saved === "dark");
}, []);
// อัปเดต DOM + localStorage เมื่อ theme เปลี่ยน
useEffect(() => {
document.documentElement.classList.toggle("dark", isDark);
localStorage.setItem("theme", isDark ? "dark" : "light");
}, [isDark]);
return (
<button onClick={() => setIsDark(!isDark)}>
{isDark ? "Light Mode" : "Dark Mode"}
</button>
);
}
Output
Timer (mount effect)
0s
useEffect(() => { ... }, [])
localStorage sync
Saved!
useEffect(() => { ... }, [text])
[] run once on mount [dep] run when dep changes none run every render

useMemo จำค่าผลลัพธ์ไว้ — คำนวณใหม่เฉพาะเมื่อ dependency เปลี่ยน เหมาะกับการคำนวณหนักหรือ filter/sort

import { useState, useMemo } from "react";
function TodoApp() {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState("all"); // "all" | "active" | "completed"
// คำนวณ filtered todos — useMemo จะ cache ผลลัพธ์
// คำนวณใหม่เฉพาะเมื่อ todos หรือ filter เปลี่ยน
const filteredTodos = useMemo(() => {
switch (filter) {
case "active":
return todos.filter((t) => !t.completed);
case "completed":
return todos.filter((t) => t.completed);
default:
return todos;
}
}, [todos, filter]);
// นับจำนวนที่ยังไม่เสร็จ
const remainingCount = useMemo(() => {
return todos.filter((t) => !t.completed).length;
}, [todos]);
return (
<div>
<p>{remainingCount} tasks remaining</p>
{/* Filter buttons */}
<div className="flex gap-2 mb-4">
{["all", "active", "completed"].map((f) => (
<button
key={f}
onClick={() => setFilter(f)}
className={filter === f ? "font-bold underline" : ""}
>
{f}
</button>
))}
</div>
{/* ใช้ filteredTodos แทน todos */}
<ul>
{filteredTodos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}

เมื่อไหร่ควรใช้ useMemo?

Section titled “เมื่อไหร่ควรใช้ useMemo?”
ใช้ไม่ต้องใช้
Filter/Sort list ใหญ่ค่าง่ายๆ เช่น count + 1
คำนวณข้อมูลจาก arrayString concatenation
แปลงข้อมูลก่อน renderค่าที่ไม่เปลี่ยนบ่อย
Output

Hookใช้เมื่อตัวอย่าง
useStateเก็บข้อมูลที่เปลี่ยนได้todos, input value, toggle
useEffectทำ side effectlocalStorage, fetch API, DOM
useMemoCache ผลลัพธ์การคำนวณfiltered list, sorted data

React component มี 3 ช่วงชีวิตหลัก — Mount (เกิด), Update (เปลี่ยนแปลง), Unmount (ตาย)

┌─────────────────────────────────────────────────────────┐
│ Mount Update Unmount │
│ (ครั้งแรก) (state/props เปลี่ยน) (ออกจากหน้า) │
│ │
│ useState(init) setState(new) cleanup() │
│ ↓ ↓ ↓ │
│ render JSX re-render JSX remove DOM │
│ ↓ ↓ │
│ useEffect(,[]) useEffect(,[dep]) │
└─────────────────────────────────────────────────────────┘
Lifecycle PhaseHook ที่เกี่ยวข้องทำอะไร
MountuseState(initialValue)กำหนดค่าเริ่มต้นของ state
MountuseEffect(() => {}, [])ทำครั้งเดียวตอน component โหลด
UpdateuseEffect(() => {}, [dep])ทำทุกครั้งที่ dependency เปลี่ยน
UnmountuseEffect(() => { return () => cleanup() }, [])ทำความสะอาดก่อน component หายไป

ตัวอย่าง: Fetch Data + Cleanup

Section titled “ตัวอย่าง: Fetch Data + Cleanup”
import { useState, useEffect } from "react";
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// ใช้ AbortController เพื่อ cancel request ตอน unmount
const controller = new AbortController();
async function fetchUser() {
setLoading(true);
try {
const res = await fetch(`/api/users/${userId}`, {
signal: controller.signal,
});
const data = await res.json();
setUser(data);
} catch (err) {
if (err.name !== "AbortError") {
console.error("Fetch failed:", err);
}
} finally {
setLoading(false);
}
}
fetchUser();
// Cleanup: cancel fetch ถ้า component unmount หรือ userId เปลี่ยน
return () => controller.abort();
}, [userId]); // ทำใหม่ทุกครั้งที่ userId เปลี่ยน
if (loading) return <p>Loading...</p>;
if (!user) return <p>User not found</p>;
return (
<div className="p-4 bg-white rounded-xl shadow">
<h2 className="text-xl font-bold">{user.name}</h2>
<p className="text-gray-500">{user.email}</p>
</div>
);
}

ตัวอย่าง: Event Listener + Cleanup

Section titled “ตัวอย่าง: Event Listener + Cleanup”
function WindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setSize({ width: window.innerWidth, height: window.innerHeight });
};
// Mount: เพิ่ม listener
window.addEventListener("resize", handleResize);
// Unmount: ลบ listener
return () => window.removeEventListener("resize", handleResize);
}, []);
return (
<p>
Window: {size.width} x {size.height}
</p>
);
}

ใน React เราเลือกแสดง UI ตามเงื่อนไขได้หลายวิธี — ไม่ต้องใช้ if แบบ template engine

วิธีที่ 1: && (Short-circuit)

Section titled “วิธีที่ 1: && (Short-circuit)”

แสดง component เมื่อเงื่อนไขเป็น true เท่านั้น

function Notification({ count }) {
return (
<div>
<h1>Dashboard</h1>
{count > 0 && (
<span className="bg-red-500 text-white px-2 py-1 rounded-full">
{count} new
</span>
)}
</div>
);
}

เลือกระหว่าง 2 ตัวเลือก

function AuthButton({ isLoggedIn }) {
return (
<nav>
{isLoggedIn ? (
<button className="btn">Logout</button>
) : (
<button className="btn btn-primary">Login</button>
)}
</nav>
);
}

เหมาะกับ loading / error states — return ก่อนถึง JSX หลัก

function DataDisplay({ data, loading, error }) {
if (loading) return <Spinner />;
if (error) return <ErrorMessage message={error} />;
if (!data || data.length === 0) return <EmptyState />;
// ถึงตรงนี้ = มี data แน่นอน
return (
<ul>
{data.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}

ตัวอย่างรวม: Component ที่มี Loading, Error, Empty, Data States

Section titled “ตัวอย่างรวม: Component ที่มี Loading, Error, Empty, Data States”
import { useState, useEffect } from "react";
function ProductList() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchProducts() {
try {
const res = await fetch("/api/products");
if (!res.ok) throw new Error("Failed to fetch");
const data = await res.json();
setProducts(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchProducts();
}, []);
// Early return pattern
if (loading) {
return (
<div className="flex justify-center p-8">
<div className="animate-spin h-8 w-8 border-4 border-blue-500 border-t-transparent rounded-full" />
</div>
);
}
if (error) {
return (
<div className="p-4 bg-red-50 text-red-600 rounded-lg">
<p className="font-bold">Error</p>
<p>{error}</p>
</div>
);
}
if (products.length === 0) {
return (
<div className="text-center p-8 text-gray-400">
<p>No products found</p>
</div>
);
}
// Data state — แสดงสินค้า
return (
<div className="grid grid-cols-2 gap-4">
{products.map((product) => (
<div key={product.id} className="p-4 bg-white rounded-xl shadow">
<h3 className="font-bold">{product.name}</h3>
<p className="text-gray-500">{product.price} baht</p>
{product.stock === 0 && (
<span className="text-xs text-red-500">Out of stock</span>
)}
</div>
))}
</div>
);
}

สรุปวิธี Conditional Rendering

Section titled “สรุปวิธี Conditional Rendering”
วิธีใช้เมื่อตัวอย่าง
&&แสดง/ซ่อน อย่างเดียว{isAdmin && <AdminPanel />}
? :เลือก 1 จาก 2{ok ? <A /> : <B />}
Early returnหลาย state (loading/error)if (loading) return <Spinner />

ใน React เราจัดการ form ด้วย Controlled Components — input ทุกตัวผูกกับ state

function SearchBar() {
const [query, setQuery] = useState("");
return (
<input
type="text"
value={query} // ค่าจาก state
onChange={(e) => setQuery(e.target.value)} // อัปเดต state ทุกครั้งที่พิมพ์
placeholder="Search..."
className="px-4 py-2 border rounded-lg w-full"
/>
);
}

จัดการหลาย Input ด้วย Object State

Section titled “จัดการหลาย Input ด้วย Object State”

แทนที่จะสร้าง useState ทีละตัว ใช้ object เดียวจัดการทั้งหมด

function SignupForm() {
const [form, setForm] = useState({
name: "",
email: "",
password: "",
});
// ใช้ name attribute เป็น key
const handleChange = (e) => {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
};
return (
<form className="space-y-3">
<input name="name" value={form.name} onChange={handleChange} placeholder="Name" />
<input name="email" value={form.email} onChange={handleChange} placeholder="Email" />
<input
name="password"
type="password"
value={form.password}
onChange={handleChange}
placeholder="Password"
/>
</form>
);
}
function LoginForm() {
const [form, setForm] = useState({ email: "", password: "" });
const [submitting, setSubmitting] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e) => {
e.preventDefault(); // ป้องกันหน้า reload!
setSubmitting(true);
try {
const res = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
});
const data = await res.json();
console.log("Success:", data);
} catch (err) {
console.error("Error:", err);
} finally {
setSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4 max-w-sm">
<input
name="email"
type="email"
value={form.email}
onChange={handleChange}
placeholder="Email"
className="w-full px-3 py-2 border rounded-lg"
/>
<input
name="password"
type="password"
value={form.password}
onChange={handleChange}
placeholder="Password"
className="w-full px-3 py-2 border rounded-lg"
/>
<button
type="submit"
disabled={submitting}
className="w-full py-2 bg-blue-600 text-white rounded-lg disabled:opacity-50"
>
{submitting ? "Logging in..." : "Login"}
</button>
</form>
);
}
function ContactForm() {
const [form, setForm] = useState({ name: "", email: "", message: "" });
const [errors, setErrors] = useState({});
const [submitted, setSubmitted] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
// ลบ error เมื่อผู้ใช้เริ่มพิมพ์ใหม่
if (errors[name]) {
setErrors((prev) => ({ ...prev, [name]: "" }));
}
};
const validate = () => {
const newErrors = {};
if (!form.name.trim()) newErrors.name = "Name is required";
if (!form.email.trim()) {
newErrors.email = "Email is required";
} else if (!/\S+@\S+\.\S+/.test(form.email)) {
newErrors.email = "Email is invalid";
}
if (!form.message.trim()) newErrors.message = "Message is required";
return newErrors;
};
const handleSubmit = async (e) => {
e.preventDefault();
const newErrors = validate();
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return; // หยุด! มี error
}
// ส่งข้อมูล...
console.log("Submitting:", form);
setSubmitted(true);
setForm({ name: "", email: "", message: "" });
};
if (submitted) {
return (
<div className="p-4 bg-green-50 text-green-700 rounded-lg">
Thank you! We'll get back to you soon.
</div>
);
}
return (
<form onSubmit={handleSubmit} className="space-y-4 max-w-md">
<div>
<input
name="name"
value={form.name}
onChange={handleChange}
placeholder="Your name"
className={`w-full px-3 py-2 border rounded-lg ${
errors.name ? "border-red-500" : "border-gray-300"
}`}
/>
{errors.name && <p className="text-red-500 text-sm mt-1">{errors.name}</p>}
</div>
<div>
<input
name="email"
type="email"
value={form.email}
onChange={handleChange}
placeholder="Email address"
className={`w-full px-3 py-2 border rounded-lg ${
errors.email ? "border-red-500" : "border-gray-300"
}`}
/>
{errors.email && <p className="text-red-500 text-sm mt-1">{errors.email}</p>}
</div>
<div>
<textarea
name="message"
value={form.message}
onChange={handleChange}
placeholder="Your message"
rows={4}
className={`w-full px-3 py-2 border rounded-lg ${
errors.message ? "border-red-500" : "border-gray-300"
}`}
/>
{errors.message && <p className="text-red-500 text-sm mt-1">{errors.message}</p>}
</div>
<button type="submit" className="w-full py-2 bg-blue-600 text-white rounded-lg">
Send Message
</button>
</form>
);
}
Output
State
name: ""
email: ""
const [form, setForm] = useState({ name: "", email: "" })

src/
components/
UserCard.jsx // PascalCase
user-card.jsx // kebab-case -- ไม่แนะนำ
userCard.jsx // camelCase -- ไม่แนะนำ
// camelCase — ตาม JavaScript convention
<Button onClick={handleClick} isDisabled={false} maxWidth={200} />
// snake_case / kebab-case -- ไม่แนะนำ
<Button on_click={handleClick} is-disabled={false} max_width={200} />

หนึ่ง Component ต่อหนึ่งไฟล์

Section titled “หนึ่ง Component ต่อหนึ่งไฟล์”
// แยกไฟล์ (แนะนำ)
components/
UserCard.jsx // export default function UserCard
UserAvatar.jsx // export default function UserAvatar
UserBadge.jsx // export default function UserBadge
// รวมหลาย component ในไฟล์เดียว (ไม่แนะนำ)
components/
User.jsx // UserCard + UserAvatar + UserBadge ทั้งหมดอยู่ในนี้

By Feature (แนะนำสำหรับโปรเจกต์ใหญ่)

src/
features/
auth/
LoginForm.jsx
SignupForm.jsx
useAuth.js
products/
ProductList.jsx
ProductCard.jsx
useProducts.js
cart/
CartPage.jsx
CartItem.jsx
useCart.js

By Type (เหมาะกับโปรเจกต์เล็ก)

src/
components/
LoginForm.jsx
ProductList.jsx
CartItem.jsx
hooks/
useAuth.js
useProducts.js
useCart.js
pages/
HomePage.jsx
ProductPage.jsx

State Management: เก็บ State ใกล้ที่ใช้ที่สุด

Section titled “State Management: เก็บ State ใกล้ที่ใช้ที่สุด”
// State อยู่ใน component ที่ใช้ (แนะนำ)
function SearchBar() {
const [query, setQuery] = useState(""); // ใช้แค่ใน SearchBar
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
// State อยู่ระดับสูงเกินไป — ทำให้ re-render ทั้ง App (ไม่แนะนำ)
function App() {
const [searchQuery, setSearchQuery] = useState(""); // ไม่ต้องอยู่ระดับ App!
return (
<div>
<Header />
<SearchBar query={searchQuery} setQuery={setSearchQuery} />
<Footer />
</div>
);
}
// DO: สร้าง array/object ใหม่
setTodos([...todos, newTodo]);
setUser({ ...user, name: "New Name" });
// DON'T: mutate state ตรง
todos.push(newTodo);
user.name = "New Name";
// DO: ใส่ dependency ที่ใช้ใน effect
useEffect(() => {
fetchUser(userId);
}, [userId]);
// DON'T: ลืม dependency หรือใส่ [] ทั้งที่ใช้ตัวแปรข้างนอก
useEffect(() => {
fetchUser(userId); // userId อาจเปลี่ยน แต่ effect ไม่รู้!
}, []);
// DO: Hook อยู่ top level เสมอ
function Profile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
if (userId) fetchUser(userId);
}, [userId]);
return <div>...</div>;
}
// DON'T: Hook อยู่ใน condition — ผิดกฎ Rules of Hooks
function Profile({ userId }) {
if (!userId) return null;
const [user, setUser] = useState(null); // Hook หลัง condition!
// ...
}
// DO: แยก component เล็กๆ ที่มีหน้าที่ชัดเจน
function ProductPage() {
return (
<div>
<ProductHeader />
<ProductGallery />
<ProductInfo />
<ProductReviews />
</div>
);
}
// DON'T: ยัดทุกอย่างใน component เดียว 500+ บรรทัด
function ProductPage() {
// ... header logic ...
// ... gallery logic ...
// ... info logic ...
// ... reviews logic ...
return (
<div>
{/* 200 lines of JSX */}
</div>
);
}

  1. Counter — สร้าง counter ด้วย useState พร้อมปุ่ม +, -, reset
  2. Todo List — เพิ่ม/ลบ/toggle todo ด้วย useState
  3. localStorage — ใช้ useEffect อ่าน/เขียน todos จาก localStorage
  4. Filter — เพิ่มปุ่ม All / Active / Completed ใช้ useMemo
  5. Dark Mode — toggle theme ด้วย useState + useEffect