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

The Context API

เมื่อ component ที่ nest ลึกหลายตัวต้องการข้อมูลเดียวกัน — theme, user ปัจจุบัน, shared state ของฟอร์ม — การส่งผ่านทุก component ตรงกลางเป็น prop (prop drilling) น่าเบื่อและรก Context ให้ ancestor ตั้งค่าที่ descendant ตัวไหนก็อ่านได้ตรง ๆ

  • setContext(key, value) — เรียกใน component ตอน initialization; ทำให้ value ใช้ได้กับทุก descendant ภายใต้ key
  • getContext(key) — เรียกใน descendant ตัวไหนก็ได้; อ่านค่าของ ancestor ที่ใกล้ที่สุดสำหรับ key
Parent.svelte
<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')"]
Context ไหลลงถึง descendant ทุกตัว

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()

src/lib/ContextConsumer.svelte
<script lang="ts">
import { getContext } from 'svelte';
const cart = getContext<{ items: string[] }>('cart');
</script>
<span data-testid="count">{cart.items.length}</span>
src/lib/ContextProviderPlain.svelte
<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 />
src/lib/ContextProviderReactive.svelte
<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 />
src/lib/context.test.ts
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 mutation

setContext/getContext เรียกเหมือนกัน mutation เดียวกัน (cart.items.push('x')) — ความต่าง เดียว คือ value ที่ส่งให้ setContext ถูกห่อด้วย $state หรือเปล่า ไม่มี error ไม่มี warning ทั้งสองแบบ: plain object ใน context ไม่ trigger re-render เลยแบบเงียบ ๆ

  • 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

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 ใน contextupdate สด ๆ กระจายถึงทุก descendant พิสูจน์แล้วด้านบนลืม wrapper ง่าย — plain object ไม่ update เลยแบบเงียบ ๆ พิสูจน์แล้วด้านบน ไม่มี error
context แทน module singletonต่อ render ต่อ tree — ปลอดภัยสำหรับ request-scoped state บน serverdescendant ของ 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

เวลารีวิวโค้ด 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 $state object ที่ตั้งไว้ครั้งเดียวตอน init ไหม? การเรียก setContext ซ้ำหลัง init เป็นสัญญาณว่าโค้ดกำลังฝืนดีไซน์แบบครั้งเดียวตอน init แทนที่จะใช้มัน
  • getContext ถูกเรียกนอก component initialization ที่ไหนไหม (ข้างใน setTimeout หรือหลัง await)? เหมือน setContext มันต้อง run ระหว่าง init แบบ synchronous ของ component — เรียกช้าเกินไปจะไม่มี “component ปัจจุบัน” ให้ผูกด้วย

🐛 หา bug ในโค้ดนี้

CartProvider.svelte
<script>
import { setContext } from 'svelte';
import CartTotal from './CartTotal.svelte';
const cart = { items: [] }; // looks fine at a glance
setContext('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: [] });

context แก้ปัญหาอะไร?
ทำไมการห่อ cart ด้วย $state ตรงนี้ถึงสำคัญ?
<script>
  let cart = $state({ items: [] });
  setContext('cart', cart);
</script>
ทำไม context ปลอดภัยกว่า module-level singleton สำหรับ request-scoped state บน server?
เกิดอะไรขึ้นกับ descendant ที่อ่าน getContext('cart').items.length หลัง addItem run?
<script>
  const cart = { items: [] };
  setContext('cart', cart);
  function addItem(item) { cart.items.push(item); }
</script>
ควรเลือก props แทน context เมื่อไร?