Study Guide: React + 3 Essential Hooks
React คืออะไร?
Section titled “React คืออะไร?”ลองเล่นก่อน!
Section titled “ลองเล่นก่อน!”<App /> คลิกที่ node ในต้นไม้เพื่อ highlight component ที่ render ออกมา
const [count, setCount] = useState(0) const [color, setColor] = useState("#3b82f6") สังเกต flash effect ตอนกด -- นั่นคือ re-render! React render component ใหม่เมื่อ state เปลี่ยน
useEffect(() => { setInterval(...) }, []) useEffect(() => { localStorage.setItem(...) }, [text]) useEffect(() => { console.log("mounted"); return () => console.log("cleanup") }, []) [] ทำครั้งเดียวตอน mount [value] ทำเมื่อ value เปลี่ยน ไม่ใส่ ทำทุกรอบ render const filtered = useMemo(() => items.filter(...), [query, category]) ลอง 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 tagfunction App() { return ( <div> <Greeting /> <Greeting /> </div> );}JSX — HTML ใน JavaScript
Section titled “JSX — HTML ใน JavaScript”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 เป็น parameterfunction 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 attributesfunction 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> );}Rendering Lists
Section titled “Rendering Lists”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> );}<App /> 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> );}useState กับ Array
Section titled “useState กับ Array”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> );}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>;}Dependency Array คืออะไร?
Section titled “Dependency Array คืออะไร?”// ไม่ใส่ [] = ทำทุกครั้งที่ render (ระวัง infinite loop!)useEffect(() => { /* ... */ });
// ใส่ [] = ทำครั้งเดียวตอน mountuseEffect(() => { /* ... */ }, []);
// ใส่ [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> );}useEffect(() => { ... }, []) useEffect(() => { ... }, [text]) [] run once on mount [dep] run when dep changes none run every render Hook #3: useMemo — Optimize Performance
Section titled “Hook #3: useMemo — Optimize Performance”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 |
| คำนวณข้อมูลจาก array | String concatenation |
| แปลงข้อมูลก่อน render | ค่าที่ไม่เปลี่ยนบ่อย |
สรุป: 3 Hooks
Section titled “สรุป: 3 Hooks”| Hook | ใช้เมื่อ | ตัวอย่าง |
|---|---|---|
useState | เก็บข้อมูลที่เปลี่ยนได้ | todos, input value, toggle |
useEffect | ทำ side effect | localStorage, fetch API, DOM |
useMemo | Cache ผลลัพธ์การคำนวณ | filtered list, sorted data |
Component Lifecycle
Section titled “Component Lifecycle”React component มี 3 ช่วงชีวิตหลัก — Mount (เกิด), Update (เปลี่ยนแปลง), Unmount (ตาย)
Mount -> Update -> Unmount
Section titled “Mount -> Update -> Unmount”┌─────────────────────────────────────────────────────────┐│ Mount Update Unmount ││ (ครั้งแรก) (state/props เปลี่ยน) (ออกจากหน้า) ││ ││ useState(init) setState(new) cleanup() ││ ↓ ↓ ↓ ││ render JSX re-render JSX remove DOM ││ ↓ ↓ ││ useEffect(,[]) useEffect(,[dep]) │└─────────────────────────────────────────────────────────┘Hooks กับ Lifecycle
Section titled “Hooks กับ Lifecycle”| Lifecycle Phase | Hook ที่เกี่ยวข้อง | ทำอะไร |
|---|---|---|
| Mount | useState(initialValue) | กำหนดค่าเริ่มต้นของ state |
| Mount | useEffect(() => {}, []) | ทำครั้งเดียวตอน component โหลด |
| Update | useEffect(() => {}, [dep]) | ทำทุกครั้งที่ dependency เปลี่ยน |
| Unmount | useEffect(() => { 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> );}Conditional Rendering
Section titled “Conditional Rendering”ใน 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: Ternary (? :)
Section titled “วิธีที่ 2: Ternary (? :)”เลือกระหว่าง 2 ตัวเลือก
function AuthButton({ isLoggedIn }) { return ( <nav> {isLoggedIn ? ( <button className="btn">Logout</button> ) : ( <button className="btn btn-primary">Login</button> )} </nav> );}วิธีที่ 3: Early Return
Section titled “วิธีที่ 3: Early Return”เหมาะกับ 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 /> |
Form Handling
Section titled “Form Handling”ใน React เราจัดการ form ด้วย Controlled Components — input ทุกตัวผูกกับ state
Controlled Input พื้นฐาน
Section titled “Controlled Input พื้นฐาน”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> );}Form Submission
Section titled “Form Submission”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> );}Basic Validation
Section titled “Basic Validation”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> );}const [form, setForm] = useState({ name: "", email: "" }) Best Practices & Style Guide
Section titled “Best Practices & Style Guide”Component Naming: PascalCase
Section titled “Component Naming: PascalCase”src/ components/ UserCard.jsx // PascalCase user-card.jsx // kebab-case -- ไม่แนะนำ userCard.jsx // camelCase -- ไม่แนะนำProps Naming: camelCase
Section titled “Props Naming: 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 ทั้งหมดอยู่ในนี้Folder Structure
Section titled “Folder Structure”By Feature (แนะนำสำหรับโปรเจกต์ใหญ่)
src/ features/ auth/ LoginForm.jsx SignupForm.jsx useAuth.js products/ ProductList.jsx ProductCard.jsx useProducts.js cart/ CartPage.jsx CartItem.jsx useCart.jsBy Type (เหมาะกับโปรเจกต์เล็ก)
src/ components/ LoginForm.jsx ProductList.jsx CartItem.jsx hooks/ useAuth.js useProducts.js useCart.js pages/ HomePage.jsx ProductPage.jsxState 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 / Don’t Comparison
Section titled “Do / Don’t Comparison”1. State Update
Section titled “1. State Update”// DO: สร้าง array/object ใหม่setTodos([...todos, newTodo]);setUser({ ...user, name: "New Name" });
// DON'T: mutate state ตรงtodos.push(newTodo);user.name = "New Name";2. useEffect Dependencies
Section titled “2. useEffect Dependencies”// DO: ใส่ dependency ที่ใช้ใน effectuseEffect(() => { fetchUser(userId);}, [userId]);
// DON'T: ลืม dependency หรือใส่ [] ทั้งที่ใช้ตัวแปรข้างนอกuseEffect(() => { fetchUser(userId); // userId อาจเปลี่ยน แต่ effect ไม่รู้!}, []);3. Conditional Hooks
Section titled “3. Conditional Hooks”// 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 Hooksfunction Profile({ userId }) { if (!userId) return null; const [user, setUser] = useState(null); // Hook หลัง condition! // ...}4. Component Size
Section titled “4. Component Size”// 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> );}ลองทำเอง
Section titled “ลองทำเอง”- Counter — สร้าง counter ด้วย
useStateพร้อมปุ่ม +, -, reset - Todo List — เพิ่ม/ลบ/toggle todo ด้วย
useState - localStorage — ใช้
useEffectอ่าน/เขียน todos จาก localStorage - Filter — เพิ่มปุ่ม All / Active / Completed ใช้
useMemo - Dark Mode — toggle theme ด้วย
useState+useEffect