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

Lifting State & Composition

เมื่อสอง component ต้องใช้ state ก้อนเดียวกัน state นั้นจะอยู่ใน component ใด component หนึ่งไม่ได้ — state นั้นต้องย้าย ขึ้น ไปที่ common ancestor ที่ใกล้ที่สุด แล้วส่งลงมาเป็น props นี่คือ lifting state up และเป็นคำตอบเริ่มต้นของ “สอง component นี้ต้อง sync กัน”

// The parent owns the state; both children read/update it through props.
function TemperatureApp() {
const [celsius, setCelsius] = useState(0);
return (
<>
<CelsiusInput value={celsius} onChange={setCelsius} />
<FahrenheitLabel celsius={celsius} />
</>
);
}

state มี single source of truth (คือ parent) และข้อมูลไหลทางเดียว: ลงมาเป็น props และย้อนขึ้นผ่าน callback สอง input ไม่มีทางขัดแย้งกันเพราะมีค่าเดียว

lifting ทำงานได้สวยงามจนกระทั่ง ancestor อยู่ ไกล เหนือ consumer มาก ตอนนั้น props ต้องร้อยผ่านทุก component ตรงกลางที่ไม่ได้สนใจค่านั้นเลย:

// theme is only used by Button, but every layer must pass it through.
<App theme={theme}>
<Page theme={theme}>
<Sidebar theme={theme}>
<Button theme={theme} /> {/* finally used here */}
</Sidebar>
</Page>
</App>

ทุก component ตรงกลางได้ prop ที่แค่ส่งต่อ นี่คือ prop drilling — รก และเป็นฝันร้ายตอน refactor สัญชาตญาณคือกระโดดไป Context ทันที แต่มีทางแก้ที่ง่ายกว่าให้ลองก่อน

ก่อนไป Context ให้ลอง composition ก่อน แทนที่จะ drill ค่าผ่าน component ให้ส่ง JSX ที่ render แล้ว เข้าไปเป็น children (หรือ prop ที่ตั้งชื่อ) เพื่อให้ component ตรงกลางไม่เห็นค่านั้นเลย

// Sidebar doesn't know about `theme` — it just renders whatever children it's given.
function Sidebar({ children }) {
return <aside className="sidebar">{children}</aside>;
}
// The Button is created where `theme` is in scope, then handed to Sidebar as content.
<Page>
<Sidebar>
<Button theme={theme} /> {/* theme is bound here, not drilled */}
</Sidebar>
</Page>
flowchart TB
  subgraph drill["Prop drilling"]
    a1["App theme"] --> p1["Page theme"] --> s1["Sidebar theme"] --> b1["Button ใช้ theme"]
  end
  subgraph comp["Composition"]
    a2["App: render Button ตรงนี้"] --> s2["Sidebar: render children"]
    a2 -.ส่ง JSX.-> s2
  end
drilling ร้อย props ผ่านไปเรื่อย ๆ ส่วน composition ส่งมอบ content ที่ render แล้ว

คุณยังส่ง JSX ผ่าน prop ที่ตั้งชื่อได้ด้วย (<Layout sidebar={<Nav />} main={<Feed />} />) component ตรงกลางจะกลายเป็น container ทั่วไป (“render อะไรก็ตามที่ถูกส่งมา”) ซึ่ง reuse ได้ดีกว่า และ กำจัด drilling ไปด้วย Context เอาไว้ใช้ตอนที่แม้แต่ composition ก็เอื้อมไม่ถึง — ข้อมูลแบบ cross-cutting จริง ๆ ที่ถูกใช้กระจัดกระจายหลายที่

เมื่อสอง component พี่น้องต้องใช้ state เดียวกัน state นั้นควรอยู่ที่ไหน?
prop drilling คืออะไร?
ทางแก้ prop drilling ด้วย composition คืออะไร?
ใน one-way data flow ข้อมูลไหลอย่างไร?