Providing Context to Child Components
In the previous article, we explored what the Context API is and why it exists. We saw how context eliminates prop drilling by allowing ancestor components to broadcast data that any descendant can receive. Now it’s time to master the mechanics of providing context—the act of establishing that broadcast.
This article focuses entirely on the provider side of the context equation. We’ll thoroughly examine setContext, understand its timing constraints and why they exist, explore what kinds of values you can provide, and develop robust patterns for building provider components that scale with your application.
By the end of this article, you’ll be able to confidently set up context in any scenario, build reusable provider components, and make informed decisions about how to structure context in your applications.
This article is intentionally thoroughYou don’t need to absorb everything on the first read — focus on the patterns and revisit as needed. This guide serves both as a learning resource and a reference you’ll return to.
The setContext Function: A Deep Dive
The setContext function is the heart of context provision. Let’s examine it thoroughly to understand not just how it works, but why it works the way it does.
Function Signature
function setContext<T>(key: any, value: T): T The function accepts two parameters:
key — The identifier for context. This can be any value that works as a JavaScript Map key: strings, Symbols, objects, numbers, or even undefined (though that would be confusing). The key is how descendant components will look up this context.
value — The actual data you’re making available. This can be literally any JavaScript value: primitives, objects, arrays, functions, class instances, Promises, Maps, Sets — anything.
Return value — The function returns exactly the same value you passed in. This might seem pointless at first, but it’s actually quite useful, as we’ll see.
setContext does not create reactivity
setContextonly stores a reference—it does not make the value reactive. If you need consumers to see updates, you must explicitly use$stateand getters. We’ll cover this pattern in detail later.
Basic Usage
At its simplest, setContext looks like this:
<script>
import { setContext } from 'svelte'
let { children } = $props()
setContext('greeting', 'Hello, world!')
</script>
{@render children()} After this code runs, any component rendered inside this one (via {@render children()}) can call getContext('greeting') to receive the string 'Hello, world!'.
Think of it like a radio broadcast: this component is now transmitting on the 'greeting' frequency, and any descendant can tune in to receive the message.
Understanding the Return Value
The fact that setContext returns its value is a subtle but useful feature. It allows you to set context and keep a local reference in a single expression:
<script>
import { setContext } from 'svelte'
// Without using return value - two lines
const config = { apiUrl: '/api', timeout: 5000 }
setContext('config', config)
// Using return value - one line, same result
const config2 = setContext('config', { apiUrl: '/api', timeout: 5000 })
// Both work identically
console.log(config === config2) // true (if they were the same call)
</script> This is particularly useful when you want to both provide context and use the value locally within the provider component:
<script>
import { setContext } from 'svelte'
// Create and provide the API client, keeping a local reference
const api = setContext(
'api',
new ApiClient({
baseUrl: 'https://api.example.com',
timeout: 10000
})
)
// Use it locally in the provider component
$effect(() => {
api.healthCheck().then((status) => {
console.log('API Status:', status)
})
})
</script> This pattern keeps your code DRY — you create the value, provide it, and keep a reference all in one expression.
Choosing the Key: Naming Your Broadcast
The first argument to setContext is the key — an identifier that descendants will use to retrieve context value. This can be any JavaScript value that works as a Map key, but your choice has implications for collision safety and developer experience.
String Keys (Simple Cases)
For small applications or isolated features, string keys work well and are easy to read:
<script>
import { setContext } from 'svelte'
setContext('theme', 'dark')
setContext('user', { name: 'Alice' })
setContext('locale', 'en-US')
</script> Using strings has the advantage of being readable, easy to type, and immediately understandable. The downside? In large applications, different parts of your codebase might accidentally use the same string key, causing collisions. Worse, a third-party library might use the same key as your application code.
Symbol Keys (Recommended for Libraries)
Use of Symbols guarantees uniqueness. Even if two different files create Symbol('theme'), they’re different symbols — because Symbol() generates a new, globally unique identifier every time it’s called, regardless of the description string you pass
// src/lib/context/keys.ts
export const THEME_KEY = Symbol('theme')
export const USER_KEY = Symbol('user')
export const CONFIG_KEY = Symbol('config') <script>
import { setContext } from 'svelte'
import { THEME_KEY } from '$lib/context/keys'
setContext(THEME_KEY, { mode: 'dark', accent: '#3b82f6' })
</script> This is the recommended approach for:
- Reusable libraries that others will install
- Large applications with multiple teams
- Any situation where you need to guarantee no key collisions
The string passed to Symbol() is just a description for debugging—it doesn’t affect uniqueness.
Object Keys (Advanced)
You can even use objects as keys, though this is rarely necessary:
<script>
import { setContext } from 'svelte'
// The object reference IS the key
const CART_KEY = {}
setContext(CART_KEY, { items: [] })
</script> This works because JavaScript Map uses reference equality for object keys. However, Symbols are usually clearer for this purpose.
When to Use Which Key Type
| Scenario | Recommended Key Type |
|---|---|
| Quick prototype or small app | String |
| Feature within your app | String or Symbol |
| Shared library/package | Symbol |
| TypeScript project | createContext (covered in detail later in this article) |
For TypeScript projectsIf you’re using TypeScript,
createContext(Svelte 5.40+) is usually the preferred solution. It eliminates manual key management and provides full type inference. We cover it in detail in the Type-Safe Context with createContext section.
What Can You Provide as Context?
You can provide any JavaScript value as context. Let’s examine different types and their implications, because your choice significantly affects how consumers interact with the data.
Primitive Values
Any primitive—string, number, boolean, null, undefined, Symbol—can be a context value:
<script>
import { setContext } from 'svelte'
setContext('appName', 'BookIt') // string
setContext('maxUploadSize', 10485760) // number (10MB in bytes)
setContext('isProduction', true) // boolean
setContext('deprecatedFeature', null) // null
setContext('experimentalFlag', undefined) // undefined (rarely useful)
</script> Important characteristic of primitives: They are passed by value, meaning the context captures a snapshot at the moment setContext is called. If you later modify the original variable, the context value doesn’t change:
<script>
import { setContext } from 'svelte'
let count = 0
setContext('count', count) // Context captures 0
count = 100 // This does NOT update the context
// Descendants still see 0
</script> Primitives are static snapshotsThis behavior is critical to understand: primitives in context are static snapshots, not live values. You can’t expect them to update automatically — this is one of the most common beginner mistakes.
If you need reactivity, you must use objects with
$state, discussed in detail in the Making Context Reactive article.
Objects and Arrays
Objects and arrays are passed by reference, meaning descendants receive the exact same object in memory:
<script>
import { setContext } from 'svelte'
const user = { name: 'Alice', score: 0 }
setContext('user', user)
// Later modifications ARE visible to descendants
user.score = 100 // Descendants see { name: 'Alice', score: 100 }
// BUT: This is a property mutation, not automatic reactivity
// Components won't re-render automatically unless you use $state
</script> This reference-sharing has important implications:
- Modifications are visible — When you modify the object, all components holding a reference see the changes
- But not automatically reactive — Svelte doesn’t know you modified it; components won’t re-render automatically
- Identity is preserved —
context.user === useristrue; they’re the same object
Objects/arrays are not automatically reactiveFor reactive context that triggers UI updates, you must use
$stateand getters. Simply mutating properties won’t cause re-renders. We’ll cover this pattern in the Making Context Reactive article.
Conceptual Checkpoint
Before we continue to more complex value types, let’s pause and consolidate what we’ve learned so far:
- Context stores references, not subscriptions —
setContextcaptures what you give it at call time. For primitives, that’s a value copy; for objects, that’s a reference to the same object in memory. - Reactivity must be explicit — Svelte doesn’t automatically track context values. If you want consumers to see updates and re-render, you need
$stateand getters. - Providers own state and Consumers observe — The component that calls
setContextis responsible for the data. Consumers receive it and react to it, but the Provider controls when and how it changes.
With this mental model established, let’s look at more powerful value types.
Functions
Functions are excellent context values, especially for providing actions. Instead of just providing data, you can provide capabilities:
<script>
import { setContext } from 'svelte'
// Simple utility function
setContext('formatPrice', (cents) => `$${(cents / 100).toFixed(2)}`)
// Logger with context
setContext('log', (level, message) => {
console.log(`[${new Date().toISOString()}] [${level.toUpperCase()}] ${message}`)
})
// Navigation helper
setContext('navigate', (path) => {
window.history.pushState({}, '', path)
window.dispatchEvent(new PopStateEvent('popstate'))
})
</script> Functions in context enable the “actions via context” pattern, where you provide not just data but capabilities:
<!-- ToastProvider.svelte -->
<script>
import { setContext } from 'svelte'
let { children } = $props()
let toasts = $state([])
function showToast(message, type = 'info', duration = 3000) {
const id = crypto.randomUUID()
toasts.push({ id, message, type })
setTimeout(() => {
toasts = toasts.filter((t) => t.id !== id)
}, duration)
}
// Provide the action, not the data
setContext('toast', showToast)
</script>
{@render children()}
<div class="toast-container">
{#each toasts as toast (toast.id)}
<div class="toast toast-{toast.type}">{toast.message}</div>
{/each}
</div> Consumers don’t need to know how toasts are stored or rendered — they just call the function:
<script>
import { getContext } from 'svelte'
const toast = getContext('toast')
function handleSave() {
// ... save logic
toast('Document saved successfully!', 'success')
}
</script> This pattern creates a clean separation: the provider owns the implementation, consumers just use the capability.
Class Instances
Class instances work perfectly as context values, enabling powerful encapsulation patterns:
<script>
import { setContext } from 'svelte'
class ApiClient {
constructor(baseUrl) {
this.baseUrl = baseUrl
}
async get(path) {
const response = await fetch(`${this.baseUrl}${path}`)
return response.json()
}
async post(path, data) {
const response = await fetch(`${this.baseUrl}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
return response.json()
}
}
// Provide a configured API client instance
setContext('api', new ApiClient('https://api.example.com'))
</script> This is particularly powerful because:
- The class encapsulates related functionality
- Configuration happens once in the provider
- All descendants share the same configured instance
- Methods have access to instance state (like authentication tokens)
Classes with $state properties become reactive services:
<script>
import { setContext } from 'svelte'
class CartService {
items = $state([])
add(product) {
this.items.push(product)
}
remove(id) {
this.items = this.items.filter((i) => i.id !== id)
}
get total() {
return this.items.reduce((sum, item) => sum + item.price, 0)
}
get count() {
return this.items.length
}
}
setContext('cart', new CartService())
</script> Maps, Sets, and Other Built-ins
Any JavaScript value works, including built-in collections:
<script>
import { setContext } from 'svelte'
// Cache using Map
setContext('cache', new Map())
// Feature flags using Set
setContext('enabledFeatures', new Set(['dark-mode', 'beta-search', 'new-checkout']))
// Regular expression for validation
setContext('emailPattern', /^[^\s@]+@[^\s@]+\.[^\s@]+$/)
// Date for "app started at"
setContext('appStartTime', new Date())
</script> What About Promises?
You can provide Promises as context, though it requires careful handling:
<script>
import { setContext } from 'svelte'
// Provide a Promise for async data
const userPromise = fetch('/api/user').then((r) => r.json())
setContext('userPromise', userPromise)
</script> Consumers would need to await it:
<script>
import { getContext } from 'svelte'
const userPromise = getContext('userPromise')
let user = $state(null)
$effect(() => {
userPromise.then((data) => {
user = data
})
})
</script>
{#if user}
<p>Welcome, {user.name}!</p>
{:else}
<p>Loading...</p>
{/if} However, this pattern has drawbacks:
- Every consumer must handle the async complexity
- Error handling is scattered across consumers
- No centralized loading state
Prefer resolving async data in the providerIn most cases, providing Promises directly is a code smell. It’s better to resolve async data in the provider and expose reactive state with loading/error handling built in. We’ll see this pattern in the provider examples.
The Timing Rule: Component Initialization
Perhaps the most important rule about setContext is when it can be called. Understanding this rule — and why it exists — will save you from confusing errors and help you design better providers.
The Rule
setContext must be called during component initialization — the synchronous execution of your <script> block when the component first renders.
What “Component Initialization” Means
When Svelte renders a component, it executes your <script> block synchronously from top to bottom. This initial, synchronous execution is “component initialization”. Once this phase completes, the component is “initialized,” and the context window closes.
<script>
import { setContext } from 'svelte'
// ═══════════════════════════════════════════
// COMPONENT INITIALIZATION PHASE
// (synchronous execution of script block)
// ═══════════════════════════════════════════
console.log('1. Start of initialization')
setContext('a', 'value') // ✅ Valid - during initialization
function helperThatSetsContext() {
setContext('b', 'value') // ✅ Valid IF called synchronously below
}
helperThatSetsContext() // Called during initialization
const result = (() => {
setContext('c', 'value') // ✅ Valid - IIFE runs during initialization
return 'done'
})()
console.log('2. End of initialization')
// ═══════════════════════════════════════════
// END OF INITIALIZATION
// Everything below runs AFTER initialization
// ═══════════════════════════════════════════
function handleClick() {
setContext('d', 'value') // ❌ Invalid - runs on click, after init
}
$effect(() => {
setContext('e', 'value') // ❌ Invalid - effects run after init
})
setTimeout(() => {
setContext('f', 'value') // ❌ Invalid - runs later, after init
}, 0)
fetch('/api').then(() => {
setContext('g', 'value') // ❌ Invalid - runs when Promise resolves
})
</script> Why This Restriction Exists
This timing restriction isn’t arbitrary — it’s fundamental to how context works. Let’s understand why by examining the component initialization sequence.
1. Context is structural, established at mount time
Context is part of the component tree’s structure, like the DOM hierarchy. When a component mounts, Svelte needs to know what context it provides so that child components can access it during their initialization.
Consider this tree:
Parent (sets 'user' context)
└── Child (reads 'user' context) The sequence is:
- Parent starts initializing
- Parent calls
setContext('user', userData) - Parent finishes initializing
- Parent renders its template (which includes Child)
- Child starts initializing
- Child calls
getContext('user')— this must work! - Child finishes initializing
If Parent set context in an event handler (after step 3), it would be too late, the Child already tried to read it in step 6.
2. Child components initialize immediately
When Svelte encounters a child component in the template, it initializes that child immediately (in the same synchronous execution). There’s no delay between “parent finishes” and “child starts.”
<!-- Parent.svelte -->
<script>
import { setContext } from 'svelte'
import Child from './Child.svelte'
console.log('Parent: before setContext')
setContext('data', { value: 42 })
console.log('Parent: after setContext')
</script>
<Child />
<!-- Output:
Parent: before setContext
Parent: after setContext
Child: reading context
Child: got { value: 42 }
--> 3. Predictability and debugging
If context could be set at any time, it would be nearly impossible to reason about whether context is available. The restriction ensures that once a component initializes, all the context from its ancestors is settled and available.
This predictability is a feature, not a limitation. You can always count on context being available (or not) based on the component tree structure.
The Error Message
If you violate the timing rule, Svelte throws:
Error: `setContext(...)` can only be called during component initialization This error is your friend—it means you’ve tried to call setContext after the initialization window closed. It catches bugs early rather than letting them manifest as confusing “undefined” values later.
Common Mistakes and Fixes
Let’s examine the most common timing mistakes and how to fix them properly.
Setting context in an event handler
<script>
import { setContext } from 'svelte'
// ❌ Wrong - handler runs after initialization
function handleInit() {
setContext('late', 'value')
}
</script>
<button onclick={handleInit}>Initialize</button> Why it fails: Event handlers run when the user interacts, long after initialization.
Fix: Set context at the top level with a reactive container, then update that container later:
<script>
import { setContext } from 'svelte'
// ✅ Set during initialization
let data = $state(null)
setContext('data', {
get value() {
return data
}
})
function handleInit() {
// Update the state, which is already in context
data = 'initialized value'
}
</script> Setting context after await
<script>
import { setContext } from 'svelte'
// ❌ Wrong - code after await runs asynchronously
async function setup() {
const response = await fetch('/api/config')
const config = await response.json()
setContext('config', config) // Too late!
}
setup()
</script> Why it fails: The await pauses execution. When it resumes, initialization is over.
Fix: Provide a reactive container synchronously, update it when data arrives:
<script>
import { setContext } from 'svelte'
// ✅ Set context synchronously with reactive state
let config = $state(null)
let loading = $state(true)
let error = $state(null)
setContext('config', {
get data() {
return config
},
get loading() {
return loading
},
get error() {
return error
}
})
// Fetch and update state (this runs async, but context is already set)
fetch('/api/config')
.then((r) => r.json())
.then((data) => {
config = data
loading = false
})
.catch((err) => {
error = err.message
loading = false
})
</script> Now consumers can check loading and error while waiting for data:
<script>
import { getContext } from 'svelte'
const config = getContext('config')
</script>
{#if config.loading}
<p>Loading configuration...</p>
{:else if config.error}
<p>Error: {config.error}</p>
{:else}
<p>API URL: {config.data.apiUrl}</p>
{/if} Setting context in $effect
<script>
import { setContext } from 'svelte'
let { userId } = $props()
// ❌ Wrong - effects run after initialization
$effect(() => {
setContext('userId', userId)
})
</script> Why it fails: Effects run after the component mounts, not during initialization.
Fix: Set context once during initialization. If the value might change, use a reactive getter:
<script>
import { setContext } from 'svelte'
let { userId } = $props()
// ✅ Set during initialization with reactive getter
setContext('user', {
get id() {
return userId
} // Always returns current prop value
})
</script> Building Provider Components
A provider is any component that calls setContext() to make data available to its descendants. In SvelteKit applications, the most natural place to provide context is in layout files, but you can also create dedicated provider components for reusability.
Providing Context in Layouts (SvelteKit)
In SvelteKit, +layout.svelte files automatically wrap all pages in their route segment. This makes them the idiomatic place to provide context:
<!-- src/routes/+layout.svelte -->
<script>
import { setContext } from 'svelte'
let { children } = $props()
// Provide theme context to all pages
setContext('theme', 'dark')
</script>
{@render children()} Every page and component in your application can now access the theme via getContext('theme'). No wrapper components needed—SvelteKit’s layout system handles the hierarchy naturally.
For route-specific context, use nested layouts:
<!-- src/routes/admin/+layout.svelte -->
<script>
import { setContext } from 'svelte'
let { children } = $props()
// Admin-specific context, only available in /admin/* routes
setContext('adminConfig', {
sidebarCollapsed: false,
permissions: ['read', 'write', 'delete']
})
</script>
{@render children()} Reusable Provider Components
For context logic you want to reuse across multiple layouts or applications, extract it into a dedicated component:
<!-- src/lib/providers/ThemeProvider.svelte -->
<script>
import { setContext } from 'svelte'
let { theme = 'light', children } = $props()
setContext('theme', theme)
</script>
{@render children()} Then use it in your layout:
<!-- src/routes/+layout.svelte -->
<script>
import ThemeProvider from '$lib/providers/ThemeProvider.svelte'
let { children } = $props()
</script>
<ThemeProvider theme="dark">
{@render children()}
</ThemeProvider> This approach separates the context logic from the layout structure, making it testable and portable.
Provider with Reactive State
Most real providers need reactive state so consumers see updates. Here’s where getters become essential:
<!-- ThemeProvider.svelte -->
<script>
import { setContext } from 'svelte'
let { initialTheme = 'light', children } = $props()
let theme = $state(initialTheme)
function setTheme(newTheme) {
theme = newTheme
}
function toggleTheme() {
theme = theme === 'light' ? 'dark' : 'light'
}
// Provide reactive getters and actions
setContext('theme', {
get current() {
return theme
},
set: setTheme,
toggle: toggleTheme
})
</script>
{@render children()} Why getters? Notice we use get current() { return theme } instead of just current: theme. This is crucial for reactivity:
// ❌ Without getter - captures value at creation time
setContext('theme', {
current: theme // This is 'light', forever frozen
})
// ✅ With getter - reads current value each time
setContext('theme', {
get current() {
return theme
} // Always returns current $state value
}) Without the getter, current would be the value of theme at the moment the object was created—it would never update. With the getter, accessing current runs the getter function, which reads the current $state value.
We’ll explore this reactivity pattern in depth in the Making Context Reactive article.
Provider with Side Effects
Providers can manage side effects related to their context. Here’s a theme provider that handles system preference detection, persistence, and DOM updates:
<!-- ThemeProvider.svelte -->
<script>
import { setContext } from 'svelte'
let { initialTheme = 'system', children } = $props()
let theme = $state(initialTheme)
let resolvedTheme = $state(initialTheme === 'system' ? getSystemTheme() : initialTheme)
function getSystemTheme() {
if (typeof window === 'undefined') return 'light'
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
// Watch for system theme changes
$effect(() => {
if (theme !== 'system') {
resolvedTheme = theme
return
}
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
function handleChange(e) {
resolvedTheme = e.matches ? 'dark' : 'light'
}
// Set initial value
resolvedTheme = mediaQuery.matches ? 'dark' : 'light'
// Listen for changes
mediaQuery.addEventListener('change', handleChange)
return () => {
mediaQuery.removeEventListener('change', handleChange)
}
})
// Apply theme to document
$effect(() => {
document.documentElement.setAttribute('data-theme', resolvedTheme)
})
// Persist to localStorage
$effect(() => {
if (typeof localStorage !== 'undefined') {
localStorage.setItem('theme', theme)
}
})
setContext('theme', {
get preference() {
return theme
},
get resolved() {
return resolvedTheme
},
setPreference(newTheme) {
theme = newTheme
},
toggle() {
if (theme === 'system') {
theme = resolvedTheme === 'light' ? 'dark' : 'light'
} else {
theme = theme === 'light' ? 'dark' : 'light'
}
}
})
</script>
{@render children()} This provider:
- Manages theme preference state (
'light','dark', or'system') - Resolves
'system'to actual light/dark based on OS preference - Responds to system theme changes in real-time
- Persists preference to localStorage
- Applies theme to the DOM via data attribute
- Exposes a clean API to consumers
Consumers don’t need to know any of these implementation details—they just use the context:
<script>
import { getContext } from 'svelte'
const theme = getContext('theme')
</script>
<p>Current theme: {theme.resolved}</p>
<button onclick={theme.toggle}>Toggle Theme</button> Provider with UI
Providers can render their own UI alongside children. This is perfect for features like toast notifications, modals, or overlays:
<!-- ToastProvider.svelte -->
<script>
import { setContext } from 'svelte'
import { fly, fade } from 'svelte/transition'
let { children } = $props()
let toasts = $state([])
function addToast(message, options = {}) {
const { type = 'info', duration = 4000 } = options
const id = crypto.randomUUID()
toasts.push({ id, message, type })
if (duration > 0) {
setTimeout(() => removeToast(id), duration)
}
return id
}
function removeToast(id) {
toasts = toasts.filter((t) => t.id !== id)
}
setContext('toast', {
show: addToast,
success: (msg, opts) => addToast(msg, { ...opts, type: 'success' }),
error: (msg, opts) => addToast(msg, { ...opts, type: 'error' }),
warning: (msg, opts) => addToast(msg, { ...opts, type: 'warning' }),
dismiss: removeToast,
dismissAll: () => {
toasts = []
}
})
</script>
{@render children()}
<!-- Toast container rendered by the provider -->
{#if toasts.length > 0}
<div class="toast-container" role="region" aria-label="Notifications">
{#each toasts as toast (toast.id)}
<div
class="toast toast-{toast.type}"
role="alert"
in:fly={{ y: 20, duration: 200 }}
out:fade={{ duration: 150 }}
>
<span class="toast-message">{toast.message}</span>
<button
class="toast-dismiss"
onclick={() => removeToast(toast.id)}
aria-label="Dismiss notification"
>
×
</button>
</div>
{/each}
</div>
{/if}
<style>
.toast-container {
position: fixed;
bottom: 1rem;
right: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
z-index: 9999;
}
.toast {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
background: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
max-width: 400px;
}
.toast-success {
border-left: 4px solid #10b981;
}
.toast-error {
border-left: 4px solid #ef4444;
}
.toast-warning {
border-left: 4px solid #f59e0b;
}
.toast-info {
border-left: 4px solid #3b82f6;
}
.toast-dismiss {
background: none;
border: none;
font-size: 1.25rem;
cursor: pointer;
opacity: 0.5;
padding: 0;
line-height: 1;
}
.toast-dismiss:hover {
opacity: 1;
}
</style> Now any component in the tree can trigger toasts without knowing about the implementation:
<script>
import { getContext } from 'svelte'
const toast = getContext('toast')
async function handleSave() {
try {
await saveDocument()
toast.success('Document saved!')
} catch (err) {
toast.error(`Failed to save: ${err.message}`)
}
}
</script>
<button onclick={handleSave}>Save</button> Composing Multiple Contexts
Real applications often need multiple contexts. In SvelteKit, you can set them all in your root layout:
<!-- src/routes/+layout.svelte -->
<script>
import { setContext } from 'svelte'
let { children } = $props()
// Set up all app-wide contexts
let theme = $state('light')
let user = $state(null)
let toasts = $state([])
setContext('theme', {
get current() {
return theme
},
toggle() {
theme = theme === 'light' ? 'dark' : 'light'
}
})
setContext('auth', {
get user() {
return user
},
get isAuthenticated() {
return user !== null
},
login(userData) {
user = userData
},
logout() {
user = null
}
})
setContext('toast', {
show(message, type = 'info') {
const id = crypto.randomUUID()
toasts.push({ id, message, type })
setTimeout(() => {
toasts = toasts.filter((t) => t.id !== id)
}, 3000)
}
})
</script>
{@render children()}
<!-- Toast UI -->
{#each toasts as toast (toast.id)}
<div class="toast toast-{toast.type}">{toast.message}</div>
{/each} For complex applications, you can extract each context into its own module and compose them:
<!-- src/routes/+layout.svelte -->
<script>
import { setupThemeContext } from '$lib/context/theme.svelte'
import { setupAuthContext } from '$lib/context/auth.svelte'
import { setupToastContext } from '$lib/context/toast.svelte'
let { children } = $props()
// Each function calls setContext internally
setupThemeContext()
setupAuthContext()
const { toasts } = setupToastContext()
</script>
{@render children()}
{#each toasts as toast (toast.id)}
<div class="toast">{toast.message}</div>
{/each} Each setupXContext function wraps context wiring, state management, and side effects so your layout can stay focused on composing providers rather than implementation details:
// src/lib/context/theme.svelte.ts
import { setContext } from 'svelte'
export function setupThemeContext(initial = 'light') {
let theme = $state(initial)
const context = {
get current() {
return theme
},
toggle() {
theme = theme === 'light' ? 'dark' : 'light'
}
}
setContext('theme', context)
return context
} This setup-function pattern shines for non-visual context and scales cleanly as apps grow, improving organization and reusability.
Context Order MattersWhen providing multiple contexts, the order of
setContextcalls matters. Child components read context during their initialization, so any context they depend on must be set up before they are rendered.
Providing Multiple Context Values
When a component needs to provide several pieces of context, you have architectural choices to make. Let’s examine the strategies and when to use each.
Strategy 1: Single Combined Context Object
Bundle related values into one context:
<script>
import { setContext } from 'svelte'
let user = $state({ name: 'Alice', email: 'alice@example.com' })
let preferences = $state({ theme: 'dark', language: 'en' })
let permissions = $state(['read', 'write'])
setContext('app', {
get user() {
return user
},
get preferences() {
return preferences
},
get permissions() {
return permissions
},
updateUser(updates) {
user = { ...user, ...updates }
},
updatePreferences(updates) {
preferences = { ...preferences, ...updates }
},
hasPermission(perm) {
return permissions.includes(perm)
}
})
</script> Advantages:
- Single import for consumers:
const app = getContext('app') - Clear what’s available: one object with everything
- Easier to ensure consistency between related values
Disadvantages:
- Less granular: any change to any value affects all consumers
- Harder to override just one piece in a subtree
- Can become unwieldy as it grows
Best for: Tightly coupled data that changes together and is consumed together.
Strategy 2: Multiple Separate Contexts
Each concern gets its own context key:
<script>
import { setContext } from 'svelte'
let user = $state({ name: 'Alice', email: 'alice@example.com' })
let preferences = $state({ theme: 'dark', language: 'en' })
let permissions = $state(['read', 'write'])
setContext('user', {
get data() {
return user
},
update(updates) {
user = { ...user, ...updates }
}
})
setContext('preferences', {
get data() {
return preferences
},
update(updates) {
preferences = { ...preferences, ...updates }
}
})
setContext('permissions', {
get list() {
return permissions
},
has(perm) {
return permissions.includes(perm)
}
})
</script> Advantages:
- Granular: components only subscribe to what they need
- Each can be overridden independently in subtrees
- Clearer separation of concerns
- Scales better as application grows
Disadvantages:
- More imports for consumers who need multiple contexts
- More keys to manage and document
Best for: Loosely coupled data that changes independently or is consumed by different components.
Strategy 3: Hybrid Approach
Combine related data, separate unrelated concerns:
<script>
import { setContext } from 'svelte'
// Auth-related (tightly coupled)
let user = $state(null)
let isAuthenticated = $derived(user !== null)
setContext('auth', {
get user() {
return user
},
get isAuthenticated() {
return isAuthenticated
},
login(userData) {
user = userData
},
logout() {
user = null
}
})
// UI preferences (separate concern)
let theme = $state('light')
let sidebarCollapsed = $state(false)
setContext('ui', {
get theme() {
return theme
},
get sidebarCollapsed() {
return sidebarCollapsed
},
setTheme(t) {
theme = t
},
toggleSidebar() {
sidebarCollapsed = !sidebarCollapsed
}
})
// Feature flags (separate concern)
let flags = $state(new Set(['beta-search']))
setContext('features', {
isEnabled(flag) {
return flags.has(flag)
},
enable(flag) {
flags.add(flag)
},
disable(flag) {
flags.delete(flag)
}
})
</script> This approach groups related data while keeping unrelated concerns separate.
Decision Guide
Use this table to decide how to structure your context:
| Question | If Yes | If No |
|---|---|---|
| Does the data change together? | Combine | Separate |
| Is it consumed together? | Combine | Separate |
| Might subtrees need different values? | Separate | Either |
| Is it growing large (>5 properties)? | Separate into logical groups | Either |
| Do different teams own different parts? | Separate | Either |
Type-Safe Context with createContext
Svelte 5.40 introduced createContext, a purpose-built API that addresses the ergonomic shortcomings of manual key management. If you’ve been following along, you’ve seen how setContext and getContext work with arbitrary keys (strings, Symbols, objects). While powerful, this approach has friction: you must manage keys yourself, TypeScript can’t infer types automatically, and missing context fails silently with undefined.
createContext solves all of these problems by generating a matched pair of getter and setter functions that share an internal, guaranteed-unique key. You never see or manage the key—it’s handled for you.
Why createContext Exists
Before createContext, the idiomatic TypeScript pattern looked like this:
// keys.ts
export const USER_KEY = Symbol('user')
// types.ts
export interface User {
id: string
name: string
role: 'admin' | 'user'
}
// provider.svelte
import { setContext } from 'svelte'
import { USER_KEY, type User } from './types'
setContext(USER_KEY, user)
// consumer.svelte
import { getContext } from 'svelte'
import { USER_KEY, type User } from './types'
const user = getContext(USER_KEY) as User // Manual type assertion! This works, but notice the pain points:
- Key management overhead — You must export the Symbol and import it everywhere
- Type assertions required —
getContextreturnsunknown, so you cast manually - Silent failures — If no provider set context, you get
undefinedwithout warning - Scattered imports — Consumers need both the key and the type
createContext eliminates all of this by bundling key generation, type inference, and runtime validation into a single API.
How createContext Works
The function signature is straightforward:
function createContext<T>(defaultValue?: T): [getter: () => T, setter: (value: T) => T] When you call createContext<T>(), Svelte:
- Generates a unique internal key (you never see it)
- Returns a getter function that retrieves context with that key
- Returns a setter function that stores context with that key
- Wires up TypeScript so both functions know the type
T
The getter and setter are bound to the same hidden key, so they always match.
Basic Usage
Here’s the complete pattern:
// lib/context/user.ts
import { createContext } from 'svelte'
export interface User {
id: string
name: string
email: string
role: 'admin' | 'user' | 'guest'
}
// createContext returns a [getter, setter] tuple
// The generic type parameter flows to both functions
export const [getUser, setUser] = createContext<User>() In your provider component, you call the setter during initialization:
<!-- UserProvider.svelte -->
<script lang="ts">
import { setUser } from '$lib/context/user'
let { user, children } = $props()
// TypeScript ensures `user` matches the User interface
// If you pass the wrong shape, you get a compile error
setUser(user)
</script>
{@render children()} In consumer components, you call the getter:
<script lang="ts">
import { getUser } from '$lib/context/user'
// No type assertion needed—TypeScript knows this is User
const user = getUser()
// Full autocomplete and type checking
console.log(user.name) // ✅ Works
console.log(user.foo) // ❌ Type error: Property 'foo' does not exist
</script>
<p>Welcome, {user.name}!</p> The key insight is that the type flows automatically. You define User once, pass it to createContext, and both the setter and getter inherit it. No manual assertions, no room for type mismatches.
Benefits Over Manual Key Management
Let’s examine each advantage in detail:
No key collisions possible
With manual keys, you’re responsible for uniqueness:
setContext('user', userData) // What if another library uses 'user'? Even with Symbols, you must ensure the same Symbol is used everywhere. With createContext, the key is internal and generated fresh each time you call createContext(). Two separate createContext() calls produce two completely independent context channels, even if they have the same type.
Full type inference without assertions
With manual keys, TypeScript can’t know what type you stored:
const user = getContext('user') as User // You're telling TypeScript to trust you Type assertions are a code smell—they bypass the compiler’s safety checks. If you change the User interface but forget to update a consumer, the assertion silently lies.
With createContext, the type is enforced at both ends:
const user = getUser() // Type is automatically User, verified by the compiler If you change the User interface, TypeScript catches any mismatches at compile time.
Throws on missing context (fail-fast behavior)
With getContext, missing context returns undefined silently:
const user = getContext('user')
// If no provider set 'user', this is undefined
// Your code might crash later with "Cannot read property 'name' of undefined" This delayed failure makes debugging harder—the error appears far from the actual problem (the missing provider).
With createContext’s getter (when no default is provided), missing context throws immediately:
const user = getUser()
// If no provider called setUser(), this throws:
// "Error: Context not found: user" The error points directly at the problem: you tried to read context that was never set. This fail-fast behavior catches bugs during development rather than in production.
Cleaner, more intentional imports
The import pattern itself communicates intent:
// Manual approach - what is USER_KEY? What type does it hold?
import { getContext } from 'svelte'
import { USER_KEY } from '$lib/context/keys'
const user = getContext(USER_KEY)
// createContext approach - the function name tells you exactly what you're getting
import { getUser } from '$lib/context/user'
const user = getUser() The getUser name is self-documenting. You don’t need to trace through key definitions to understand what context you’re accessing.
Providing Default Values
Sometimes you want context to be optional and components should work even if no provider exists. createContext supports this with an optional default value:
export const [getConfig, setConfig] = createContext<Config>({
apiUrl: '/api',
debug: false,
maxRetries: 3
}) Now the behavior changes:
- If an ancestor called
setConfig(customConfig),getConfig()returns that custom config - If no ancestor set context,
getConfig()returns the default instead of throwing
This is useful for configuration that should have sensible defaults but can be overridden:
<script lang="ts">
import { getConfig } from '$lib/context/config'
// Works even without a ConfigProvider in the tree
const config = getConfig()
</script>
<p>API URL: {config.apiUrl}</p> Default values are staticThe default value is captured when
createContextis called. If you need the default to be reactive or computed, you’ll need a different approach—typically a provider that’s always present in your layout.
Combining createContext with Reactive State
createContext handles the key management and typing, but it doesn’t make values reactive on its own. For reactive context, you combine it with $state and getters, just as with manual keys.
Here’s a complete pattern that’s become idiomatic in Svelte 5 TypeScript projects:
// lib/context/counter.svelte.ts
import { createContext } from 'svelte'
// 1. Define the context interface
// Using readonly for values that consumers shouldn't mutate directly
export interface CounterContext {
readonly count: number
increment(): void
decrement(): void
reset(): void
}
// 2. Create the typed getter/setter pair
export const [getCounter, setCounter] = createContext<CounterContext>()
// 3. Factory function that creates and provides the context
export function createCounterContext(initial = 0) {
let count = $state(initial)
// setCounter returns what you pass it, so we can return it from the factory
return setCounter({
get count() {
return count
},
increment() {
count++
},
decrement() {
count--
},
reset() {
count = initial
}
})
} This pattern has three parts:
- Interface — Defines the shape of your context, with
readonlyfor values consumers shouldn’t mutate - createContext call — Generates the typed getter/setter
- Factory function — Creates the reactive state and calls the setter
The factory function is called during component initialization:
<!-- CounterProvider.svelte -->
<script lang="ts">
import { createCounterContext } from '$lib/context/counter.svelte'
let { initial = 0, children } = $props()
// Creates state and provides context in one call
createCounterContext(initial)
</script>
{@render children()} Consumers get full type safety and reactivity:
<script lang="ts">
import { getCounter } from '$lib/context/counter.svelte'
const counter = getCounter()
</script>
<!-- Reactive: updates when count changes -->
<p>Count: {counter.count}</p>
<button onclick={counter.increment}>+</button>
<button onclick={counter.decrement}>-</button>
<button onclick={counter.reset}>Reset</button> When to Use createContext vs Manual Keys
Use createContext when:
- You’re using TypeScript and want type safety
- You want fail-fast behavior for missing context
- You prefer self-documenting imports (
getUser()vsgetContext(USER_KEY)) - You’re building a library and want to guarantee no key collisions
Stick with manual keys when:
- You’re not using TypeScript
- You need the key to be a specific value (rare, but possible for interop)
- You’re working with existing code that already uses manual keys
For new TypeScript projects, createContext is the recommended default.
Context Organization Patterns
As your application grows, how you organize context code matters for maintainability. Let’s explore common patterns.
Pattern 1: Co-located with Feature
Keep context files alongside the feature they serve:
src/features/
├── auth/
│ ├── context.svelte.ts # Auth context definition
│ ├── AuthProvider.svelte # Provider component
│ ├── LoginForm.svelte
│ ├── LogoutButton.svelte
│ └── index.ts # Public exports
├── cart/
│ ├── context.svelte.ts
│ ├── CartProvider.svelte
│ ├── CartDrawer.svelte
│ └── index.ts Advantages:
- Context is close to its consumers
- Easy to understand feature boundaries
- Self-contained, portable features
Best for: Feature-based architecture, micro-frontends, large teams.
Pattern 2: Centralized Context Directory
All context in one location:
src/lib/context/
├── auth.svelte.ts
├── cart.svelte.ts
├── theme.svelte.ts
├── toast.svelte.ts
└── index.ts # Re-exports all contexts Advantages:
- Easy to see all available context
- Simple imports:
import { getAuth, getCart } from '$lib/context' - Good for smaller applications
Best for: Smaller applications, solo developers, rapid prototyping.
Pattern 3: Context + Provider Separation
Separate the context definition from the provider component:
src/lib/
├── context/
│ ├── auth.ts # Types and createContext
│ ├── cart.ts
│ └── index.ts
├── providers/
│ ├── AuthProvider.svelte
│ ├── CartProvider.svelte
│ └── AppProviders.svelte # Composes all providers Advantages:
- Clear separation of concerns
- Context types can be imported without component overhead
- Provider components can be tested independently
Best for: Medium to large applications with complex providers.
Conclusion
Providing context is fundamentally an act of architectural declaration. When you call setContext, you’re not just passing data—you’re establishing a contract with every descendant component: “This information is available here, and you can depend on it.” The timing constraint (initialization only) isn’t a limitation but a feature that ensures this contract is reliable and predictable.
The patterns explored in this article—from simple value provision to complex providers with reactive state and side effects—all serve the same goal: making data available to components that need it without creating coupling through intermediate components. Whether you’re providing a simple configuration object or a fully-featured service with methods, effects, and UI, the fundamental mechanism remains the same.
The introduction of createContext in Svelte 5.40 represents a maturation of these patterns, eliminating the key management overhead and providing type safety out of the box. But even without it, the combination of Symbol keys, factory functions, and typed interfaces gives you the tools to build robust, maintainable context systems.
With these provider patterns mastered, the next step is understanding how to consume context effectively—handling missing context gracefully, building reusable utilities, and avoiding the subtle reactivity pitfalls that can trip up even experienced developers.
Key Takeaways
Providing context effectively requires understanding several key concepts:
setContext(key, value)establishes context that descendants can access. It returns the value you passed, useful for keeping local references.Any JavaScript value can be provided: primitives, objects, functions, class instances, and more. Objects are shared by reference; primitives are static snapshots.
Timing is critical:
setContextmust be called during component initialization—the synchronous execution of your script block. Event handlers, effects, and async code run too late.Use getters for reactivity:
get current() { return theme }reads the current value each time;current: themecaptures the initial value forever.Provider components encapsulate context setup. They can include reactive state, side effects, and even their own UI (like toast containers).
Multiple context values can be organized as a single combined object (for tightly coupled data) or separate contexts (for independent concerns).
createContext(Svelte 5.40+) eliminates key management, provides type safety, and throws on missing context for early error detection.Organization patterns range from co-located with features to centralized directories, depending on your application’s size and architecture.
What’s Next
Now that you can provide context, learn how to receive it in your components in Consuming Context, covering getContext, missing context handling, and practical access patterns.
See Also
Official Documentation
- setContext API — API reference
- createContext API — Type-safe context
- Svelte Context — Official guide
Related Articles
- What is the Context API? — Context fundamentals
- Making Context Reactive — Reactive context patterns
- $state Rune — Reactive state fundamentals