ข้ามไปยังเนื้อหา

State Snapshot & Purity

นี่คือเรื่องที่คนเข้าใจผิดมากที่สุดใน React ตอนคุณอ่านตัวแปร state คุณกำลังอ่านค่าของตัวเอง สำหรับ render นี้ — snapshot ที่ถูกแช่แข็ง ณ ตอนที่ component ถูกเรียก ไม่เปลี่ยนกลาง render แม้หลังจากคุณเรียก setter แล้ว

function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// count is 0 for this whole render. All three read the SAME snapshot (0),
// so this sets state to 0 + 1 = 1, three times. Result: 1, not 3.
}
return <button onClick={handleClick}>{count}</button>;
}

ถ้าจะ update จากค่า ล่าสุด ที่ pending อยู่ ให้ส่ง updater function — React รัน updater เหล่านั้นตามลำดับกับค่าที่กำลังเปลี่ยน:

setCount((c) => c + 1);
setCount((c) => c + 1);
setCount((c) => c + 1);
// Each receives the previous pending value: 0→1→2→3. Result: 3.

Mental model: re-render คือการเรียกฟังก์ชันของคุณใหม่พร้อม snapshot ใหม่ ตัวแปรของ render เก่าหายไปแล้ว render ใหม่เห็นค่าใหม่

เพราะ state เป็น snapshot ที่ React เทียบด้วย identity คุณจึง ต้องไม่ mutate state ให้สร้าง object/array ใหม่แทน:

// ❌ Mutation — same array reference, React may not re-render, and it's a bug.
todos.push(newTodo);
setTodos(todos);
// ✅ New reference — React sees a change and re-renders.
setTodos([...todos, newTodo]);

การ mutate state ทำลายความสามารถของ React ในการตรวจจับการเปลี่ยนแปลง (React เทียบ reference) และทำลาย model แบบ snapshot ให้สร้างค่าใหม่เสมอ

การ render ของ component — ทุกอย่างที่ทำระหว่างคำนวณ JSX ที่จะ return — ต้อง pure: เมื่อ props และ state เท่าเดิม ต้อง return output เดิมและไม่มี side effect ระหว่าง render คุณต้องไม่:

  • mutate props, state หรือตัวแปร/object ใด ๆ ที่มีอยู่ก่อน;
  • เขียน DOM, ยิง network request หรือเริ่ม timer;
  • อ่านหรือเขียนอะไรก็ตามนอกฟังก์ชันที่เปลี่ยนได้
flowchart LR
  inputs["props + state
(snapshot)"] --> render["render (pure):
compute JSX only"]
  render --> out["same inputs → same JSX"]
  render -. NOT here .-> se["side effects:
DOM, network, timers"]
  se --> eff["→ event handlers & effects"]
render ที่ pure เข้า side effect ออก

Side effect อยู่ใน event handler (สิ่งที่เกิดตอน interaction) และ effect (useEffect สำหรับ sync กับระบบภายนอก) — ไม่ใช่ใน body ของ render purity นี้คือสิ่งที่ทำให้ React เรียก component ของคุณเมื่อไรก็ได้

ใน development <StrictMode> ของ React เรียกฟังก์ชัน component ของคุณสองครั้ง โดยตั้งใจ (และรัน effect setup→cleanup→setup) นี่ไม่ใช่ bug — เป็นตัวตรวจจับ ถ้า render ของคุณ pure การเรียกสองครั้งจะให้ output เหมือนกันและไม่มีอะไรพัง ถ้าคุณเผลอ mutate อะไรหรือพึ่ง side effect ระหว่าง render การเรียกซ้ำจะเผยให้เห็น bug ทันทีใน development แทนที่จะเป็น glitch ลึกลับใน production

ใน event handler เดียว เรียก `setCount(count + 1)` สามครั้งโดย count = 0 ผลลัพธ์คืออะไร?
จะ increment state สามครั้งใน handler เดียวให้ถูกต้องอย่างไร?
ทำไมต้องไม่ mutate state (เช่น `todos.push(x); setTodos(todos)`)?
การ double-render ของ StrictMode มีไว้ทำอะไร?