Published 12/5/2025 · 6 min read
Tags: svelte , context , state-management
Lesson 12: Context API
Props pass data explicitly. Stores share data globally. Context sits between — sharing data through a component tree without drilling props through every level.
The Problem Context Solves
Imagine a form with nested components:
Form
└── FieldGroup
└── Field
└── Input
└── ErrorMessage
If Form manages validation state, you’d need to pass it through every component. Tedious and fragile.
Context lets Form provide data that any descendant can access directly.
Setting Context
Use setContext in a parent component:
<!-- Form.svelte -->
<script>
import { setContext } from 'svelte'
import { writable } from 'svelte/store'
const errors = writable({})
const values = writable({})
setContext('form', {
errors,
values,
register: (name, validators) => { /* ... */ },
validate: () => { /* ... */ }
})
</script>
<form>
<slot />
</form>
setContext takes a key (any value, often a string or symbol) and a value to share.
Getting Context
Use getContext in any descendant:
<!-- ErrorMessage.svelte -->
<script>
import { getContext } from 'svelte'
let { name } = $props()
const { errors } = getContext('form')
let error = $derived($errors[name])
</script>
{#if error}
<span class="error">{error}</span>
{/if}
The child doesn’t need to know how many levels up Form is.
Context Keys
String keys work but can collide. For library code, use symbols:
// context-keys.js
export const FORM_KEY = Symbol("form");
export const THEME_KEY = Symbol("theme");
<script>
import { setContext } from 'svelte'
import { FORM_KEY } from './context-keys.js'
setContext(FORM_KEY, { /* ... */ })
</script>
Symbols guarantee uniqueness.
Context vs Stores
When to use which?
Use stores when:
- State is truly global (theme, auth, cart)
- Unrelated components need the same data
- State should persist across navigation
Use context when:
- State is scoped to a component tree
- Multiple instances might exist (multiple forms on a page)
- You’re building a component library
A form’s validation state shouldn’t be global. Multiple forms could exist. Use context.
User authentication affects the whole app. Use a store.
Context with Stores
The best pattern: put stores in context.
<!-- Tabs.svelte -->
<script>
import { setContext } from 'svelte'
import { writable } from 'svelte/store'
let { initial = 0 } = $props()
const activeTab = writable(initial)
setContext('tabs', {
activeTab,
registerTab: () => { /* ... */ }
})
</script>
<div class="tabs">
<slot />
</div>
<!-- Tab.svelte -->
<script>
import { getContext } from 'svelte'
let { id } = $props()
const { activeTab } = getContext('tabs')
let isActive = $derived($activeTab === id)
</script>
<button
class:active={isActive}
onclick={() => $activeTab = id}
>
<slot />
</button>
The store inside context gets reactivity. The context scopes it to this tab group.
hasContext
Check if context exists:
<script>
import { hasContext, getContext } from 'svelte'
const hasForm = hasContext('form')
// Only try to get context if it exists
const form = hasForm ? getContext('form') : null
</script>
Useful for optional integration with parent components.
Comparing to Vue
Vue’s provide/inject:
<!-- Parent.vue -->
<script setup>
import { provide, ref } from "vue";
const theme = ref("light");
provide("theme", theme);
</script>
<!-- Child.vue -->
<script setup>
import { inject } from "vue";
const theme = inject("theme");
</script>
Svelte:
<!-- Parent.svelte -->
<script>
import { setContext } from 'svelte'
import { writable } from 'svelte/store'
const theme = writable('light')
setContext('theme', theme)
</script>
<!-- Child.svelte -->
<script>
import { getContext } from 'svelte'
const theme = getContext('theme')
</script>
<p>Theme: {$theme}</p>
Similar concepts. Vue’s ref is reactive by default. Svelte needs a store for reactivity in context.
Practical Example: Toast System
<!-- ToastProvider.svelte -->
<script>
import { setContext } from 'svelte'
import { writable } from 'svelte/store'
const toasts = writable([])
let id = 0
function addToast(message, type = 'info', duration = 3000) {
const toast = { id: id++, message, type }
toasts.update(t => [...t, toast])
setTimeout(() => {
removeToast(toast.id)
}, duration)
}
function removeToast(id) {
toasts.update(t => t.filter(toast => toast.id !== id))
}
setContext('toasts', {
add: addToast,
remove: removeToast,
toasts
})
</script>
<slot />
<div class="toast-container">
{#each $toasts as toast (toast.id)}
<div class="toast {toast.type}">
<p>{toast.message}</p>
<button onclick={() => removeToast(toast.id)}>×</button>
</div>
{/each}
</div>
<style>
.toast-container {
position: fixed;
top: 1rem;
right: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.toast {
padding: 1rem;
border-radius: 4px;
background: #333;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
min-width: 200px;
}
.toast.success { background: #16a34a; }
.toast.error { background: #dc2626; }
.toast.warning { background: #d97706; }
</style>
Usage anywhere in the app:
<script>
import { getContext } from 'svelte'
const { add: addToast } = getContext('toasts')
async function handleSubmit() {
try {
await saveData()
addToast('Saved successfully!', 'success')
} catch (e) {
addToast('Failed to save', 'error')
}
}
</script>
Practical Example: Form Validation
<!-- FormProvider.svelte -->
<script>
import { setContext } from 'svelte'
import { writable } from 'svelte/store'
const values = writable({})
const errors = writable({})
const touched = writable({})
const validators = {}
function register(name, validate = () => null) {
validators[name] = validate
values.update(v => ({ ...v, [name]: '' }))
}
function setValue(name, value) {
values.update(v => ({ ...v, [name]: value }))
touched.update(t => ({ ...t, [name]: true }))
validateField(name, value)
}
function validateField(name, value) {
const error = validators[name]?.(value)
errors.update(e => ({ ...e, [name]: error }))
return !error
}
function validateAll() {
let valid = true
const currentValues = {}
values.subscribe(v => Object.assign(currentValues, v))()
for (const [name, value] of Object.entries(currentValues)) {
if (!validateField(name, value)) {
valid = false
}
}
return valid
}
setContext('form', {
values,
errors,
touched,
register,
setValue,
validateAll
})
</script>
<slot />
<!-- FormField.svelte -->
<script>
import { getContext } from 'svelte'
let {
name,
label = name,
type = 'text',
required = false
} = $props()
const { values, errors, touched, register, setValue } = getContext('form')
$effect(() => {
register(name, (value) => {
if (required && !value) return `${label} is required`
return null
})
})
let value = $derived($values[name] || '')
let error = $derived($errors[name])
let showError = $derived($touched[name] && error)
</script>
<div class="field">
<label for={name}>{label}</label>
<input
{type}
id={name}
{value}
oninput={(e) => setValue(name, e.target.value)}
/>
{#if showError}
<span class="error">{error}</span>
{/if}
</div>
Key Takeaways
setContext(key, value)shares data with descendantsgetContext(key)retrieves that data- Use symbols for keys in libraries to avoid collisions
- Put stores in context for reactivity
- Context is scoped; stores are global
- Use
hasContextto check before getting - Perfect for component libraries (tabs, forms, modals)
Related Articles
- Svelte 5 Runes: A Complete Guide for Vue Developers
A comprehensive guide to Svelte 5's runes system. Learn $state, $derived, $effect, $props, and $bindable with side-by-side Vue comparisons.
- Compressed NFTs: Collections, Verification, and Building a Claim Page
Taking our cNFT minting system to production: creating verified collections, building a web-based claim flow, and preparing for mainnet deployment.
- Adding Drizzle ORM to Your SvelteKit + SQLite Setup
Level up from raw SQL to type-safe queries with Drizzle—the lightweight ORM that doesn't hide the SQL you've learned.