Skip to main content

React Hooks

useState​

const [state, setState] = useState(initialValue);
🧠 จำง่ายๆ

"useState คืนกล่อง 2 ช่อง: ช่องอ่านค่า กับ ช่องขอเปลี่ยนค่า" — state คือค่าปัจจุบัน (อ่าน อย่างเดียว) setState คือฟังก์ชัน "ขอ" ให้ React เปลี่ยนค่าแล้ว re-render ให้ (ไม่ใช่เปลี่ยน ค่าทันทีแบบ synchronous)

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

useState(expensiveComputation()) ที่ argument เป็นการเรียกฟังก์ชันตรงๆ จะถูก เรียกทุก ครั้งที่ component render แม้ผลลัพธ์จะถูกใช้แค่ครั้งแรก (initial value) เท่านั้น! React จะ "ทิ้ง" ผลลัพธ์ที่คำนวณซ้ำนั้นไป แต่ก็ยังเสีย CPU คำนวณฟรีๆ ทุก render อยู่ดี วิธีแก้คือใช้ lazy initialization โดยส่งฟังก์ชันเข้าไปแทนค่าตรงๆ: useState(() => expensiveComputation()) — React จะเรียกฟังก์ชันนี้แค่ครั้งเดียวตอน mount เท่านั้น เป็น optimization เล็กๆ ที่คนจำนวนมากไม่รู้ว่ามีอยู่

useEffect​

useEffect(() => {
const subscription = subscribeToSomething();
return () => subscription.unsubscribe(); // cleanup function
}, [dependency]); // dependency array
🧠 จำง่ายๆ

"useEffect = จดหมายสั่งงานหลัง render เสร็จ" — โค้ดข้างใน useEffect รันหลัง browser วาด หน้าจอเสร็จแล้วเท่านั้น (ไม่บล็อก render) ส่วน dependency array คือ "เงื่อนไขว่าต้องส่งจดหมาย ใหม่เมื่อไหร่" — ถ้าค่าใน array เปลี่ยน ถึงจะรัน effect ใหม่

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

Dependency array ว่างเปล่า [] ไม่ได้แปลว่า "รันครั้งเดียวตลอดชีวิต component" ในความหมาย ที่ปลอดภัยเสมอ — มันหมายถึง "effect นี้ไม่ depend ค่าอะไรจาก render scope เลย" ถ้าโค้ดข้างใน effect อ้างอิงตัวแปรจาก closure (เช่น prop หรือ state) แต่ไม่ใส่ไว้ใน dependency array จะเกิด stale closure bug — effect จะ "เห็น" ค่าเก่าตลอดไปจากตอน mount ครั้งแรก ไม่อัปเดตตามค่า ใหม่เลย แม้ component จะ re-render ด้วยค่าใหม่แล้วก็ตาม ESLint plugin eslint-plugin-react-hooks มี rule exhaustive-deps ที่เตือนเรื่องนี้โดยเฉพาะ — การปิด warning นี้ทิ้งโดยไม่เข้าใจสาเหตุคือที่มาของบั๊ก "ทำไมค่าไม่อัปเดต" ที่พบบ่อยที่สุดในโค้ด React

useRef​

const inputRef = useRef(null);
// inputRef.current ชี้ไปยัง DOM element จริงหลัง render
🧠 จำง่ายๆ

"useRef = กล่องเก็บของที่เปลี่ยนแล้วไม่ทำให้บ้าน (component) re-render" — ต่างจาก useState ที่เปลี่ยนค่าแล้ว "เรียกช่าง" มาทาสีบ้านใหม่ (re-render) useRef แค่เปลี่ยนของในกล่องเงียบๆ โดยไม่บอกใคร เหมาะกับเก็บค่าที่ไม่ต้องการให้กระทบ UI (เช่น timer ID, DOM reference)

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

useRef ใช้เก็บค่า "ที่ไม่เกี่ยวกับ DOM" ได้ด้วย ไม่ใช่แค่ reference DOM element — เป็น pattern ที่มีประโยชน์มากสำหรับเก็บ "ค่าล่าสุดแบบ mutable ที่ไม่อยากให้ trigger re-render" เช่น เก็บ previous value เพื่อเทียบกับค่าปัจจุบัน:

function usePrevious(value) {
const ref = useRef();
useEffect(() => { ref.current = value; });
return ref.current; // ค่าจาก render ก่อนหน้า (ไม่ใช่ปัจจุบัน)
}

เทคนิคนี้ใช้แก้ปัญหา stale closure ได้ด้วย: เก็บค่าล่าสุดไว้ใน ref แล้วให้ callback อ่านจาก ref.current แทนที่จะ capture ค่าจาก closure ตรงๆ — ref จะ "เห็น" ค่าล่าสุดเสมอไม่ว่า closure จะถูกสร้างตอนไหนก็ตาม

useMemo / useCallback​

const sortedList = useMemo(() => expensiveSort(list), [list]);
const handleClick = useCallback(() => doSomething(id), [id]);
🧠 จำง่ายๆ

"useMemo จำ 'ผลลัพธ์', useCallback จำ 'ตัวฟังก์ชันเอง'" — useMemo cache ค่าที่คำนวณได้ useCallback cache "ตัวอ้างอิง" ของฟังก์ชัน (ไม่ให้สร้างฟังก์ชันใหม่ทุก render) จริงๆ useCallback(fn, deps) เทียบเท่ากับ useMemo(() => fn, deps) เป๊ะๆ แค่เป็น syntax sugar เฉพาะสำหรับ caching function

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

useMemo/useCallback ไม่ใช่ "ของฟรี" — มันแลก memory (เก็บค่าเก่าไว้เทียบ) กับ CPU time ที่ไม่ต้องคำนวณซ้ำ ถ้าใช้พร่ำเพรื่อกับ computation ที่เบามากอยู่แล้ว (เช่น a + b) ต้นทุนของการเทียบ dependency array ทุก render อาจแพงกว่าแค่คำนวณใหม่ตรงๆ เสียอีก React core team เองเคยแนะนำว่า "ไม่ต้อง wrap ทุกอย่างด้วย useMemo" — ควรใช้เมื่อ (1) computation หนักจริงๆ (วัดได้จาก profiler) หรือ (2) ผลลัพธ์ถูกส่งต่อเป็น prop/dependency ให้ component ลูกที่ทำ React.memo ไว้ (เพื่อไม่ให้ reference เปลี่ยนจนทำให้ memo ไม่มีผล) — ใช้แบบ "กันไว้ก่อนทุกจุด" (premature optimization) จะทำให้โค้ดอ่านยากขึ้นโดยไม่ได้ performance เพิ่มจริง

Rules of Hooks​

Hook ต้องเรียก ที่ top level ของ component เท่านั้น (ไม่เรียกใน if/for/nested function) และเรียกตามลำดับเดิมทุก render

🧠 จำง่ายๆ

"Hook ต้องเข้าคิวตามลำดับเดิมทุกครั้ง เหมือนเพลงในเทปคาสเซ็ต" — React จำ state ของแต่ละ hook ด้วย ลำดับที่เรียก ไม่ใช่ชื่อตัวแปร ถ้าเรียกข้ามลำดับ (เช่น มี hook บางตัวถูกข้ามใน condition) ลำดับจะเพี้ยนและ React จะจับคู่ state ผิดตัวทันที

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

เหตุผลเชิงลึกที่ React บังคับกฎนี้: React เก็บ hook state เป็น linked list ภายใน fiber ของแต่ละ component ไม่ใช่ object ที่ map ด้วยชื่อ — แต่ละครั้งที่ component render React จะ "เดิน" ไปตาม linked list นี้ทีละ node ตามลำดับที่โค้ดเรียก useState/useEffect/... ถ้า render หนึ่งเรียก hook ไม่ตรงลำดับกับ render ก่อนหน้า (เช่น เพราะมี early return หรือ hook อยู่ใน if) node ที่ควรจะเป็นของ hook ตัวที่ 3 อาจถูกอ่านเป็นของ hook ตัวที่ 2 แทน ทำให้ ค่า state ของทั้งสอง hook สลับกันแบบเงียบๆ โดยไม่มี error ชัดเจน (บางครั้ง React จะ warn "Rendered more/fewer hooks than expected" แต่ไม่ใช่ทุกกรณี) — การเข้าใจว่า hook state เก็บ แบบ linked list ตามลำดับ ไม่ใช่ตาม "ชื่อ" คือกุญแจที่อธิบายว่าทำไมกฎนี้ถึง "ต้อง" เป็นแบบนี้ ไม่ใช่แค่ "best practice" เฉยๆ

English Summary

useState's lazy initializer (useState(() => expensive())) avoids recomputing an expensive initial value on every render. useEffect's dependency array determines when it reruns — an empty array doesn't mean "run once safely," it means "this effect reads nothing from render scope," and omitting a referenced variable causes the classic stale-closure bug (the exhaustive-deps ESLint rule exists specifically for this). useRef holds mutable values without triggering re-renders — useful for storing a "previous value" or working around stale closures by reading ref.current instead of a captured variable. useMemo/useCallback trade memory for avoided recomputation and aren't free — overusing them on cheap computations can cost more than it saves; reserve them for expensive work or stabilizing references passed into React.memo'd children. The Rules of Hooks exist because React tracks hook state as an ordered linked list per component (not a name-keyed map) — calling hooks conditionally can silently misalign state between hooks rather than always producing an obvious error.