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

Rendering & Reconciliation

ความเข้าใจผิดที่พบบ่อย: “re-render” แปลว่า React แตะ DOM ไม่ใช่ React แบ่งงานเป็นสอง phase ที่ต่างกันชัดเจน:

flowchart LR
  trigger["state change /
parent re-render"] --> render["Render phase
(call components, build new tree)
— pure, no DOM"]
  render --> recon["Reconciliation
(diff new tree vs old)"]
  recon --> commit["Commit phase
(apply minimal DOM changes)
— side effects here"]
สอง phase ของการ update
  1. Render phase — React เรียก component ของคุณเพื่อสร้าง tree ของ element ใหม่ นี่คือการคำนวณล้วน ๆ: ไม่แตะ DOM และ React หยุดหรือทิ้งงานนี้ได้
  2. Commit phase — React เทียบ tree ใหม่กับอันก่อนหน้า คำนวณชุดการแก้ DOM ที่ น้อยที่สุด แล้ว apply

ดังนั้น re-render ที่ให้ output เหมือนเดิมจะทำให้ DOM เปลี่ยน ศูนย์ จุด การ render ค่อนข้างถูก ส่วนที่แพง (แตะ DOM) เกิดเฉพาะกับสิ่งที่เปลี่ยนจริงเท่านั้น

การเทียบ tree ใด ๆ สองอันขึ้นชื่อว่าแพง (O(n³)) React ทำให้เร็ว (O(n)) ด้วย heuristic สองข้อ:

  • type ต่างกัน → ทิ้ง ถ้า type ของ element เปลี่ยน (<div> กลายเป็น <span> หรือ ComponentA กลายเป็น ComponentB) React จะ unmount subtree เก่าทั้งอันแล้วสร้างใหม่ทั้งหมด — รวมถึงทำลาย state ของตัวเองด้วย
  • type เดียวกัน → เก็บ node ไว้ update props ถ้า type เหมือนเดิม React เก็บ DOM node เดิมและ state ของตัวเองไว้ แล้ว update แค่ props ที่เปลี่ยน
// Same type across renders → the <input> keeps its DOM node and its focus/value.
{isEditing ? <input value={text} /> : <input value={text} disabled />}
// Different type → React destroys the first and mounts the second (state lost).
{isEditing ? <input value={text} /> : <p>{text}</p>}

สำหรับ list ของ sibling ชนิดเดียวกัน React ต้องรู้ว่าไอเทมไหนเป็นไอเทมไหนข้าม render โดย default React จับคู่ด้วย ตำแหน่ง (index) — ซึ่งพังทันทีที่ไอเทม reorder แทรก หรือลบ key ให้ identity ที่คงที่กับแต่ละไอเทม เพื่อให้ React ติดตามแต่ละไอเทมได้

// ✅ Stable identity — React follows each todo even when the list reorders.
{todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)}
// ⚠️ Index as key — reorder/insert/delete and React mismatches items,
// corrupting per-item state (a checked box jumps to the wrong row).
{todos.map((todo, i) => <TodoItem key={i} todo={todo} />)}
flowchart TB
  before["Before: [A, B, C]
keys a,b,c"] -->|prepend Z| after["After: [Z, A, B, C]
keys z,a,b,c"]
  after --> good["With stable keys:
A,B,C reused, Z mounted"]
  before2["Before: [A, B, C]
key = index 0,1,2"] -->|prepend Z| after2["After: index 0,1,2,3"]
  after2 --> bad["With index keys:
every item mismatched,
state shifts to wrong rows"]
key ทำให้ reconciliation ติดตามไอเทมข้ามการ reorder

กฎ: key ต้อง คงที่ ไม่ซ้ำในกลุ่ม sibling และผูกกับ identity ของข้อมูล (ปกติคือ database id) — อย่าใช้ array index สำหรับ list ที่เปลี่ยนได้ และอย่าใช้ Math.random() (ซึ่งเปลี่ยนทุก render และบังคับให้ remount ทั้งหมด)

ระหว่าง "render phase" เกิดอะไรขึ้น?
ระหว่าง reconciliation React ทำอะไรเมื่อ type ของ element เหมือนเดิม?
ทำไมการใช้ array index เป็น key ถึงเสี่ยงสำหรับ list ที่ reorder ได้?
ถ้า re-render ให้ output เหมือนเดิมเป๊ะ DOM เปลี่ยนกี่จุด?