React ทำงานอย่างไรข้างใน
โมดูลนี้มีไว้ทำไม
หัวข้อที่มีชื่อว่า “โมดูลนี้มีไว้ทำไม”ทุกโมดูลก่อนหน้าในคอร์สนี้สอนให้คุณ ใช้ React: hook, state, Context, Suspense, Server Components ไม่มีอันไหนต้องรู้เลยว่า React ทำอะไรกับ function call ของคุณหลังจากคุณเขียนมันไป คุณเขียน useEffect ที่ทำงานได้ เขียน list ที่มี key ถูกต้อง เขียน <Suspense> boundary ได้ — โดยไม่เคยต้องถามว่า React ตัดสินใจ เมื่อไร component function ของคุณจะรัน, ทำไม update หนึ่งถึง interrupt อีกอันได้, หรือ จริง ๆ แล้ว เกิดอะไรขึ้นระหว่าง “ฉันเรียก setState” กับ “หน้าจอเปลี่ยน”
ช่องว่างนั้นคือจุดที่โค้ด React ที่ AI เขียนพังในแบบที่ผิวเผินดูโอเค: component ที่ประกาศไว้ข้างในอีก component หนึ่ง, index ที่ใช้เป็น key ของ list ที่ reorder ได้, useLayoutEffect ที่สู้กับ browser, useEffect ที่สมมติว่ารันหลัง paint เสมอ ไม่มีอันไหนเป็น syntax error ไม่มีอันไหนที่ tsc จับได้ ทุกอันเป็นข้อเท็จจริงเกี่ยวกับ runtime ล้วน ๆ
โมดูลนี้มีงานเดียว: ให้ความรู้ runtime ห้าชิ้นที่ทำให้คุณมองโค้ด React ของคนอื่น — ไม่ต้องรัน ไม่ต้อง profile แค่อ่าน — แล้วตอบได้ว่า:
- โค้ดนี้จะ remount หรือ update ในที่เดิม? (fiber, work loop, reconciliation)
- โค้ดนี้จะบล็อกการกดคีย์ครั้งถัดไปไหม หรือ React interrupt มันได้? (lane, scheduling)
- ระหว่าง child สองตัวที่หน้าตาคล้ายกัน อันไหนเก็บ state ไว้? (กฎ type+key ของ reconciliation)
- ref,
useInsertionEffect,useLayoutEffect,useEffectของฉันรันตามลำดับไหนจริง ๆ? (commit phase) - ทำไมหน้าเว็บถึงเพิ่งโชว์ hydration error และ React ทำอะไรกับมัน? (hydration และ streaming)
ทุก claim ในโมดูลนี้อ้างอิงหนึ่งในสามอย่าง: react.dev, React source จริงบน github.com/facebook/react (ดึงจาก branch main อ้างชื่อไฟล์และ function) หรือ test ใน harness ของคอร์สนี้ที่วาง output จริงไว้ข้างล่าง — ไม่มีการเดา internals เด็ดขาด
โมดูลนี้ครอบคลุมอะไรบ้าง
หัวข้อที่มีชื่อว่า “โมดูลนี้ครอบคลุมอะไรบ้าง”| บท | ตอบคำถาม |
|---|---|
| Fiber & the Work Loop | fiber คืออะไร และ React เดิน tree เพื่อสร้างมันอย่างไร? |
| Lanes & Scheduling | React ตัดสิน priority ของ update อย่างไร และ startTransition เปลี่ยนมันอย่างไร? |
| Reconciliation in Depth | จาก tree เก่ากับ tree ใหม่ React เก็บ ย้าย หรือทิ้ง node ไหนบ้างกันแน่? |
| Commit Phase & Effects | พอ React ตัดสินใจแล้วว่าอะไรเปลี่ยน มันแตะ DOM และรัน effect ของคุณตามลำดับไหน? |
| Hydration & Streaming | หน้าที่ render จาก server กลายเป็น interactive ได้อย่างไร และเกิดอะไรขึ้นเมื่อ client กับ server ไม่ตรงกัน? |
บท Rendering & Reconciliation พิสูจน์ไปแล้วจากมุมภายนอก (render/commit แยกกัน, กฎ type-vs-key ผ่าน mount/unmount count) โมดูลนี้ไปลึกกว่านั้นถึงกลไกที่ผลิตตัวเลขพวกนั้นออกมา และลิงก์กลับแทนที่จะพิสูจน์ซ้ำ Transitions โชว์ API ฝั่งผู้ใช้ของ useTransition ไปแล้ว Lanes & Scheduling โชว์ว่า “lane” ข้างใต้มันคืออะไรกันแน่
คำถามที่รันได้จริง: component นี้ update ในที่เดิม หรือเริ่มใหม่?
หัวข้อที่มีชื่อว่า “คำถามที่รันได้จริง: component นี้ update ในที่เดิม หรือเริ่มใหม่?”ก่อนจะลงลึกกลไก นี่คือข้อเท็จจริงข้อเดียวที่ทั้งโมดูลนี้หมุนรอบ: React ตัดสิน “ของเดิม update มันไป” กับ “ของใหม่ ทิ้งแล้วสร้างใหม่” ด้วยการเทียบ type ของ element กับ fiber ที่นั่งอยู่ตำแหน่งนั้นอยู่แล้ว — ไม่มีอะไรมากไปกว่านั้น
import { useEffect } from 'react';import { render, cleanup } from '@testing-library/react';import { afterEach, expect, test } from 'vitest';
afterEach(cleanup);
test('same type across renders: no mount, no unmount, just a prop update', () => { const log: string[] = []; function Row({ label }: { label: string }) { useEffect(() => { log.push(`mount:${label}`); return () => { log.push(`unmount:${label}`); }; }, []); return <li>{label}</li>; } const { rerender } = render(<Row label="a" />); log.length = 0; rerender(<Row label="b" />); // same component, new prop expect(log).toEqual([]); // proves: React updated the existing fiber in place});✓ same type across renders: no mount, no unmount, just a prop update
Test Files 1 passed (1) Tests 1 passed (1)ทุกบทในโมดูลนี้จริง ๆ แล้วคือคำตอบของ “ทำไมกฎนั้นถึงเป็นจริง และมีอะไรตามมาจากมันอีก”
flowchart TD trigger["Update triggered (setState, startTransition, hydration)"] --> lane["Lane assigned — priority (Lanes & Scheduling)"] lane --> render["Render phase beginWork top-down / completeWork bottom-up interruptible via shouldYield (Fiber & the Work Loop)"] render --> recon["Reconciliation diff by type, then by key (Reconciliation in Depth)"] recon --> commit["Commit phase before-mutation -> mutation -> layout -> PAINT -> passive (Commit Phase & Effects)"] commit --> current["Work-in-progress tree becomes 'current'"] entry["Server sends HTML"] --> stream["renderToPipeableStream ... hydrateRoot selective hydration, mismatch recovery (Hydration & Streaming)"] stream --> render
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”การอ่านโค้ด React ที่ไม่คุ้นเคยให้ดี คือการรันเครื่องเล็ก ๆ ในหัวคุณ ในขณะที่ตาเห็นแค่ JSX: element นี้ type อะไร, มี key อะไร, update นี้อยู่ lane ไหน, และตอนนี้อยู่ phase ไหนของ commit ห้าบทในที่นี้คือสี่คำถามนั้นเป๊ะ ๆ บทละหนึ่งคำถาม (fiber/work-loop คลุม “type อะไร และ React ไปถึงตรงนั้นได้อย่างไร”; lanes คลุม “priority อะไร”; reconciliation คลุม “key เดียวกัน = instance เดียวกัน”; commit คลุม “phase ไหน ลำดับอะไร”; hydration คลุมสิ่งที่เกิดขึ้นเมื่อ render แรกสุดมี DOM ให้ reconcile ด้วยอยู่แล้ว)
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “internals มีไว้สำหรับคนที่อยาก contribute ให้ React” คุณต้องรู้แค่พอจะ อ่าน diff ได้ ไม่ต้องถึงขั้น reimplement reconciler ทุกข้อเท็จจริงในโมดูลนี้คือสิ่งที่คุณชี้ไปที่ pull request ของคนอื่นได้
- “ถ้า compile ผ่านและ render ถูกครั้งแรก โค้ดก็โอเคแล้ว” component ที่ประกาศข้างในอีก component,
useLayoutEffectที่วัดค่าก่อน ref จะ attach, index key บน list ที่ reorder ได้ — ทั้งหมดนี้ render ถูกใน pass แรก บั๊กจะโผล่เฉพาะ render ที่สอง ซึ่งเป็น pass เดียวกับที่การเทสด้วยมือเร็ว ๆ มักข้ามไป - “พฤติกรรม internal ของ React เป็นเวทมนตร์ที่ไม่มีเอกสาร” เกือบทุกอย่างในโมดูลนี้ระบุไว้บน react.dev หรืออ่านได้ตรง ๆ ใน
packages/react-reconciler— มันไม่คุ้นเคย ไม่ใช่ว่าไม่มีเอกสาร
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”จุดประสงค์ทั้งหมดของโมดูลนี้คือ section นี้ ซ้ำห้าครั้ง — บทละหนึ่ง เป็นตัวอย่างนำร่อง นี่คือเวอร์ชันสำรวจภาพรวม: โค้ดที่ render ถูกในครั้งแรก แต่ยังคงผิดอยู่ดี
🐛 หา bug ในโค้ดนี้
function ProductList({ products }) {function ProductCard({ product }) { return <li>{product.name}</li>;}return ( <ul> {products.map((p) => <ProductCard key={p.id} product={p} />)} </ul>);}เฉลย
ProductCard ถูกประกาศ ข้างใน render body ของ ProductList ทุกครั้งที่ ProductList render มันสร้าง function ใหม่เอี่ยม — type ใหม่ — ทั้งที่โค้ดหน้าตาเหมือนเดิมทุกประการ reconciler ของ React เทียบ type ของ element (ตัว function reference เอง) กับสิ่งที่อยู่ตรงนั้นก่อนหน้า function reference ใหม่แปลว่า “นี่คือ component คนละตัว” ดังนั้นทุก ProductCard จะ unmount แล้ว remount ทุกครั้งที่ ProductList re-render ไม่ว่าจะมี key หรือไม่ก็ตาม state ข้างใน ProductCard reset ทุกครั้ง ทางแก้คือย้าย ProductCard ออกไปไว้ top level ของ module (ข้างนอก ProductList) เพื่อให้ type เป็น function reference เดียวกันข้ามทุก render Fiber & the Work Loop กับ Reconciliation in Depth พิสูจน์กลไกนี้ด้วย test จริง