Skip to content

The Context API

When many nested components need the same data — a theme, the current user, a form’s shared state — passing it through every intermediate component as props (prop drilling) is tedious and noisy. Context lets an ancestor set a value that any descendant can read directly.

  • setContext(key, value) — called in a component during initialization; makes value available to all descendants under key.
  • getContext(key) — called in any descendant; reads the nearest ancestor’s value for 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 flows down to any descendant

setContext runs once, at init — the value it stores isn’t reactive by itself. To share reactive data, put a runed object into context and read its properties in descendants; because $state objects are deeply reactive, changes propagate.

<!-- 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>

A common pattern is a small factory that creates the runed state and pairs setContext/getContext behind typed helper functions, so consumers just call getCart().

Proving the difference: a plain object vs a $state object

Section titled “Proving the difference: a plain object vs a $state object”
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

Same setContext/getContext calls, same mutation (cart.items.push('x')) — the only difference is whether the value passed to setContext was wrapped in $state. No error, no warning either way: a plain object in context silently never triggers a re-render.

  • Props — best for direct parent→child data. Explicit and easy to trace.
  • Context — best when data is needed by many descendants at varying depths (theme, auth, a widget’s shared state) and is scoped to a subtree.
  • Module state (.svelte.js) — best for truly app-global state, but mind SSR (a module singleton is shared across requests on the server — the state-management module covers this).

Crucially, context is per component tree and per render, so on the server each request gets its own context — making it the SSR-safe way to share request-scoped state, unlike a module-level singleton.

Context is a read channel bound to the component tree, not a value store of its own — setContext just registers “here’s the value for this key, for whoever renders below me.” Whether that value is reactive is entirely up to what you put in it: a plain object is a frozen snapshot handed down once; a $state object is a live reference descendants read from, proven above. Context itself never re-runs, re-checks, or diffs anything — all the reactivity, if any, comes from the value’s own runes.

What you getBenefitCost
Context over prop drillingNo threading a value through components that don’t use itLess explicit than props — a descendant’s data source isn’t visible at its call site
$state object in contextLive updates propagate to every descendant, proven aboveEasy to forget the wrapper — a plain object silently never updates, proven above, with no error
Context over a module singletonPer-render, per-tree — safe for request-scoped state on the serverOnly reachable by descendants of the component that called setContext; siblings/ancestors can’t read it
  • “Putting any object in setContext makes it reactive.” It doesn’t — proven above, a plain object mutated after setContext produces zero UI update, silently. Only a $state-wrapped value is live.
  • “If context isn’t reactive, Svelte will warn you.” It won’t — the two test cases above run identically except for one $state() wrapper, and only one of them fails to update, with no console output either way.
  • “Context is basically a global variable.” It’s scoped to a component tree and to a single render — the state-and-stores module covers why that specifically matters for SSR, where a real module-level singleton would leak between requests.

When reviewing generated context code, check for:

  • Is the value passed to setContext a plain object/array literal ({ items: [] }, []) that later gets mutated, expecting descendants to see the update? Proven above: without $state, they won’t.
  • Does a “cart” / “auth” / “theme” provider component reassign the context value on every change (setContext('cart', newCart) inside an event handler) instead of mutating a $state object once set at init? Repeated setContext calls after init are a sign the code is fighting the one-time-at-init design instead of using it.
  • Is getContext called anywhere outside component initialization (inside a setTimeout, or after an await)? Like setContext, it must run during a component’s synchronous init — called too late, there’s no “current component” to attach to.

🐛 Spot the 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 />
Show the answer

cart is a plain object literal, never wrapped in $state. Calling addItem genuinely pushes into cart.items — the data is correct — but because cart isn’t reactive, <CartTotal> (which reads it via getContext('cart')) never re-renders to reflect the new total, proven above with an identical repro. There’s no error and no warning; the total just silently stays wrong until something unrelated causes a re-render. The fix is one word: const cart = $state({ items: [] });.

What problem does context solve?
Why does wrapping cart in $state here matter?
<script>
  let cart = $state({ items: [] });
  setContext('cart', cart);
</script>
Why is context safer than a module-level singleton for request-scoped state on the server?
What happens in a descendant reading getContext('cart').items.length after addItem runs?
<script>
  const cart = { items: [] };
  setContext('cart', cart);
  function addItem(item) { cart.items.push(item); }
</script>
When should you prefer props over context?