React Hooks
useState
const [state, setState] = useState(initialValue);
"useState คืนกล่อง 2 ช่อง: ช่องอ่านค่า กับ ช่องขอเปลี่ยนค่า" — state คือค่าปัจจุบัน (อ่าน
อย่างเดียว) setState คือฟังก์ชัน "ขอ" ให้ React เปลี่ยนค่าแล้ว re-render ให้ (ไม่ใช่เปลี่ยน
ค่าทันทีแบบ synchronous)
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 ใหม่
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)
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
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 ผิดตัวทันที
เหตุผลเชิงลึกที่ 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" เฉยๆ
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.