The Context API
Context หลีกเลี่ยง prop drilling
หัวข้อที่มีชื่อว่า “Context หลีกเลี่ยง prop drilling”เมื่อ component ที่ nest ลึกหลายตัวต้องการข้อมูลเดียวกัน — theme, user ปัจจุบัน, shared state ของฟอร์ม — การส่งผ่านทุก component ตรงกลางเป็น prop (prop drilling) น่าเบื่อและรก Context ให้ ancestor ตั้งค่าที่ descendant ตัวไหนก็อ่านได้ตรง ๆ
setContext(key, value)— เรียกใน component ตอน initialization; ทำให้valueใช้ได้กับทุก descendant ภายใต้keygetContext(key)— เรียกใน descendant ตัวไหนก็ได้; อ่านค่าของ ancestor ที่ใกล้ที่สุดสำหรับkey
<script> import { setContext } from 'svelte'; setContext('theme', 'dark'); let { children } = $props();</script>{@render children()}<!-- DeepChild.svelte (any depth below) --><script> import { getContext } from 'svelte'; const theme = getContext('theme'); // 'dark' — no props threaded through</script><div class={theme}>…</div>flowchart TB
parent["Ancestor: setContext('theme', value)"] --> mid["intermediate components
(no props needed)"]
mid --> child["Descendant: getContext('theme')"] Reactive context ด้วย runes
หัวข้อที่มีชื่อว่า “Reactive context ด้วย runes”setContext run ครั้งเดียวตอน init — value ที่มันเก็บไม่ reactive ด้วยตัวเอง ถ้าจะแชร์ข้อมูลแบบ reactive ให้ใส่ runed object เข้า context แล้วอ่าน property ของมันใน descendant เพราะ $state object reactive แบบ deep การเปลี่ยนแปลงจึงกระจายไปได้
<!-- Store a runed object, not a plain snapshot --><script> import { setContext } from 'svelte'; let cart = $state({ items: [] }); setContext('cart', cart); // descendants see live updates to cart.items</script>pattern ที่พบบ่อยคือ factory เล็ก ๆ ที่สร้าง runed state แล้วผูก setContext/getContext ไว้หลัง typed helper function เพื่อให้ consumer เรียกแค่ getCart()
พิสูจน์ความต่าง: plain object กับ $state object
หัวข้อที่มีชื่อว่า “พิสูจน์ความต่าง: plain object กับ $state object”<script lang="ts"> import { getContext } from 'svelte'; const cart = getContext<{ items: string[] }>('cart');</script><span data-testid="count">{cart.items.length}</span><script lang="ts"> import { setContext } from 'svelte'; import ContextConsumer from './ContextConsumer.svelte'; const cart = { items: [] as string[] }; // BUG: plain object, not reactive setContext('cart', cart);</script><button data-testid="add" onclick={() => cart.items.push('x')}>add</button><ContextConsumer /><script lang="ts"> import { setContext } from 'svelte'; import ContextConsumer from './ContextConsumer.svelte'; const cart = $state({ items: [] as string[] }); // fixed: runed state setContext('cart', cart);</script><button data-testid="add" onclick={() => cart.items.push('x')}>add</button><ContextConsumer />import { test, expect } from 'vitest';import { render, fireEvent } from '@testing-library/svelte';import ContextProviderPlain from './ContextProviderPlain.svelte';import ContextProviderReactive from './ContextProviderReactive.svelte';
test('a plain object in context does NOT update the UI on mutation', async () => { const { container } = render(ContextProviderPlain); const count = () => container.querySelector('[data-testid="count"]')!.textContent; expect(count()).toBe('0'); await fireEvent.click(container.querySelector('[data-testid="add"]')!); // still '0': the descendant never re-renders because a plain object isn't reactive expect(count()).toBe('0');});
test('a $state object in context DOES update the UI on mutation', async () => { const { container } = render(ContextProviderReactive); const count = () => container.querySelector('[data-testid="count"]')!.textContent; expect(count()).toBe('0'); await fireEvent.click(container.querySelector('[data-testid="add"]')!); expect(count()).toBe('1');});✓ a plain object in context does NOT update the UI on mutation✓ a $state object in context DOES update the UI on mutationsetContext/getContext เรียกเหมือนกัน mutation เดียวกัน (cart.items.push('x')) — ความต่าง เดียว คือ value ที่ส่งให้ setContext ถูกห่อด้วย $state หรือเปล่า ไม่มี error ไม่มี warning ทั้งสองแบบ: plain object ใน context ไม่ trigger re-render เลยแบบเงียบ ๆ
Context เทียบ props เทียบ global state
หัวข้อที่มีชื่อว่า “Context เทียบ props เทียบ global state”- Props — เหมาะกับข้อมูล parent→child ตรง ๆ ชัดเจนและ trace ง่าย
- Context — เหมาะเมื่อข้อมูลถูกต้องการโดย descendant หลายตัว ที่ความลึกต่างกัน (theme, auth, shared state ของ widget) และ scope อยู่ที่ subtree
- Module state (
.svelte.js) — เหมาะกับ state ที่ app-global จริง ๆ แต่ต้องระวัง SSR (module singleton ถูกแชร์ข้าม request บน server — โมดูล state-management พูดถึงเรื่องนี้)
ที่สำคัญคือ context เป็น ต่อ component tree และต่อ render ดังนั้นบน server แต่ละ request ได้ context ของตัวเอง — ทำให้มันเป็นวิธีแชร์ request-scoped state ที่ SSR-safe ต่างจาก module-level singleton
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”Context คือ read channel ที่ผูกกับ component tree ไม่ใช่ value store ของตัวมันเอง — setContext แค่ลงทะเบียนว่า “นี่คือ value สำหรับ key นี้ สำหรับใครก็ตามที่ render อยู่ข้างล่างฉัน” value นั้น reactive หรือไม่ ขึ้นอยู่กับ สิ่งที่คุณใส่เข้าไป ทั้งหมด: plain object คือ snapshot ที่แช่แข็งส่งลงไปครั้งเดียว; $state object คือ reference ที่มีชีวิตที่ descendant อ่านจากมันจริง ๆ พิสูจน์แล้วด้านบน context เองไม่เคย run ซ้ำ, เช็คซ้ำ, หรือ diff อะไรเลย — reactivity ทั้งหมดถ้ามี มาจาก rune ของ value เอง
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ได้อะไร | ประโยชน์ | ต้นทุน |
|---|---|---|
| context แทน prop drilling | ไม่ต้องร้อย value ผ่าน component ที่ไม่ได้ใช้มัน | ชัดเจนน้อยกว่า props — แหล่งข้อมูลของ descendant ไม่เห็นที่ call site |
$state object ใน context | update สด ๆ กระจายถึงทุก descendant พิสูจน์แล้วด้านบน | ลืม wrapper ง่าย — plain object ไม่ update เลยแบบเงียบ ๆ พิสูจน์แล้วด้านบน ไม่มี error |
| context แทน module singleton | ต่อ render ต่อ tree — ปลอดภัยสำหรับ request-scoped state บน server | descendant ของ component ที่เรียก setContext เท่านั้นที่เข้าถึงได้; sibling/ancestor อ่านไม่ได้ |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “ใส่ object อะไรก็ได้ใน
setContextแล้วมันจะ reactive” ไม่จริง — พิสูจน์แล้วด้านบน plain object ที่ mutate หลังsetContextทำให้ UI update เป็นศูนย์ แบบเงียบ ๆ มีแค่ value ที่ห่อด้วย$stateเท่านั้นที่มีชีวิตจริง - “ถ้า context ไม่ reactive Svelte จะเตือนคุณ” ไม่จริง — สอง test case ด้านบน run เหมือนกันทุกอย่างยกเว้น wrapper
$state()หนึ่งตัว และมีแค่อันเดียวที่ update ไม่ได้ โดยไม่มี console output เลยทั้งคู่ - “context ก็แค่ global variable” context scope อยู่ที่ component tree และ render เดียว โมดูล state-and-stores พูดถึงว่าทำไมสิ่งนี้สำคัญกับ SSR โดยเฉพาะ ที่ module-level singleton จริง ๆ จะรั่วข้าม request
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เวลารีวิวโค้ด context ที่ generate มา ให้เช็ค:
- value ที่ส่งให้
setContextเป็น plain object/array literal ({ items: [] },[]) ที่ถูก mutate ทีหลัง โดยคาดหวังว่า descendant จะเห็น update ไหม? พิสูจน์แล้วด้านบน: ถ้าไม่มี$stateจะไม่เห็น - component provider แบบ “cart” / “auth” / “theme” reassign context value ทุกครั้งที่เปลี่ยน (
setContext('cart', newCart)ข้างใน event handler) แทนที่จะ mutate$stateobject ที่ตั้งไว้ครั้งเดียวตอน init ไหม? การเรียกsetContextซ้ำหลัง init เป็นสัญญาณว่าโค้ดกำลังฝืนดีไซน์แบบครั้งเดียวตอน init แทนที่จะใช้มัน getContextถูกเรียกนอก component initialization ที่ไหนไหม (ข้างในsetTimeoutหรือหลังawait)? เหมือนsetContextมันต้อง run ระหว่าง init แบบ synchronous ของ component — เรียกช้าเกินไปจะไม่มี “component ปัจจุบัน” ให้ผูกด้วย
🐛 หา bug ในโค้ดนี้
<script>import { setContext } from 'svelte';import CartTotal from './CartTotal.svelte';
const cart = { items: [] }; // looks fine at a glancesetContext('cart', cart);
function addItem(item) { cart.items.push(item); // mutating the "shared" cart}</script>
<button onclick={() => addItem({ name: 'Tea', price: 2 })}>Add tea</button><CartTotal />เฉลย
cart เป็น plain object literal ไม่เคยถูกห่อด้วย $state การเรียก addItem push เข้า cart.items จริง — ข้อมูลถูกต้อง — แต่เพราะ cart ไม่ reactive <CartTotal> (ที่อ่านมันผ่าน getContext('cart')) ไม่เคย re-render เพื่อสะท้อนยอดรวมใหม่เลย พิสูจน์แล้วด้านบนด้วย repro แบบเดียวกันเป๊ะ ไม่มี error ไม่มี warning เลย ยอดรวมก็แค่ผิดเงียบ ๆ ต่อไปจนกว่าจะมีอะไรอื่นที่ไม่เกี่ยวข้องทำให้ re-render วิธีแก้มีคำเดียว: const cart = $state({ items: [] });