Building on the Fundamentals
In the previous article, you learned the fundamentals: why plain context isn’t reactive, how $state with objects fixes it, and the critical “mutate, don’t reassign” rule. You can now create context that updates everywhere.
But knowing the mechanics is just the beginning. Professional context APIs need more: they need to be encapsulated (internal state protected from misuse), computed (derived values that stay in sync automatically), and effectful (persisting to storage, syncing with external systems).
This article teaches the patterns that transform basic reactive context into production-quality APIs:
- Getters for clean APIs — How to expose reactive values while hiding implementation details
- $derived for computed values — Automatic calculations that update when dependencies change
- $effect for side effects — Persistence, synchronization, and DOM updates
- Context in .svelte.ts files — Reusable, type-safe context modules
By the end, you’ll be able to build context systems that are a pleasure to use and maintain.
Getters for Clean APIs
When you expose a $state object directly via context, consumers can read and write anything. That’s fine for simple cases, but problematic as complexity grows:
<!-- Provider -->
<script>
let state = $state({ count: 0, step: 1 })
setContext('counter', state)
</script>
<!-- Any consumer can do whatever they want -->
<script>
const counter = getContext('counter')
// Direct mutation — works but bypasses any validation
counter.count = 999
counter.step = -1
// Consumers could even add random properties
counter.hackedValue = 'oops'
</script> This isn’t necessarily wrong, but it makes your code harder to reason about. Who’s changing what? Where do bugs come from?
The Getter Pattern
Instead of exposing raw state, expose an API object with getters for reading and methods for writing:
<!-- CounterProvider.svelte -->
<script>
import { setContext } from 'svelte'
let { initial = 0, step = 1, min = -Infinity, max = Infinity, children } = $props()
// Internal state — not directly exposed
let count = $state(initial)
// Public API with getters and methods
setContext('counter', {
// Getters: reading goes through these
get value() {
return count
},
get canIncrement() {
return count + step <= max
},
get canDecrement() {
return count - step >= min
},
// Methods: writing goes through these
increment() {
if (count + step <= max) {
count += step
}
},
decrement() {
if (count - step >= min) {
count -= step
}
},
set(newValue) {
// Validation in one place
count = Math.max(min, Math.min(max, newValue))
},
reset() {
count = initial
}
})
</script>
{@render children()} Consumers get a clean, constrained API:
<script>
import { getContext } from 'svelte'
const counter = getContext('counter')
</script>
<div class="counter">
<button onclick={counter.decrement} disabled={!counter.canDecrement}> − </button>
<span>{counter.value}</span>
<button onclick={counter.increment} disabled={!counter.canIncrement}> + </button>
<button onclick={counter.reset}>Reset</button>
</div> Why Getters Are Essential
Without getters, you might accidentally capture stale values:
let count = $state(0)
// ❌ Without getters — captures value at creation time
setContext('counter', {
value: count, // This is 0, forever
canIncrement: count < max, // This is true, forever
increment() {
count++
}
})
// ✅ With getters — evaluates fresh each time
setContext('counter', {
get value() {
return count
}, // Always current
get canIncrement() {
return count < max
}, // Always current
increment() {
count++
}
}) The getter function runs every time the property is accessed. Each render, each event handler, each $derived that reads counter.value gets the current value of count.
Getters Enable Encapsulation
With getters, you can change internal implementation without breaking consumers:
// Version 1: Simple counter
let count = $state(0)
setContext('counter', {
get value() {
return count
},
increment() {
count++
}
})
// Version 2: Now we want to track history
// Changed implementation, same API
let history = $state([0])
setContext('counter', {
get value() {
return history[history.length - 1]
}, // Same API!
increment() {
history.push(history[history.length - 1] + 1)
},
undo() {
// New feature
if (history.length > 1) history.pop()
}
}) Consumers using counter.value continue working without modification. This is the power of encapsulation.
Providing Both State and Actions
A well-designed context API typically has three parts:
setContext('feature', {
// 1. State getters — what you can read
get data() {
return data
},
get isLoading() {
return isLoading
},
get error() {
return error
},
// 2. Derived getters — computed values
get isEmpty() {
return data.length === 0
},
get summary() {
return computeSummary(data)
},
// 3. Action methods — what you can do
async fetch() {
/* ... */
},
add(item) {
/* ... */
},
remove(id) {
/* ... */
},
reset() {
/* ... */
}
}) This structure makes context self-documenting. Consumers immediately understand what’s available and how to use it.
Computed Values with $derived
When your context needs computed values that depend on state, use $derived. Derived values automatically recalculate when their dependencies change, keeping everything in sync without manual effort.
Basic $derived in Context
<!-- CartProvider.svelte -->
<script>
import { setContext } from 'svelte'
let { children } = $props()
// Core state
let items = $state([])
let discountPercent = $state(0)
// Derived values — computed automatically
let itemCount = $derived(items.length)
let totalQuantity = $derived(items.reduce((sum, item) => sum + item.quantity, 0))
let subtotal = $derived(items.reduce((sum, item) => sum + item.price * item.quantity, 0))
let discountAmount = $derived(subtotal * (discountPercent / 100))
let total = $derived(subtotal - discountAmount)
let isEmpty = $derived(items.length === 0)
// Context API
setContext('cart', {
// State
get items() {
return items
},
// Derived values
get itemCount() {
return itemCount
},
get totalQuantity() {
return totalQuantity
},
get subtotal() {
return subtotal
},
get discountAmount() {
return discountAmount
},
get total() {
return total
},
get isEmpty() {
return isEmpty
},
// Methods
addItem(product, quantity = 1) {
const existing = items.find((i) => i.id === product.id)
if (existing) {
existing.quantity += quantity
} else {
items.push({
id: product.id,
name: product.name,
price: product.price,
quantity
})
}
},
removeItem(id) {
const index = items.findIndex((i) => i.id === id)
if (index !== -1) {
items.splice(index, 1)
}
},
applyDiscount(percent) {
discountPercent = Math.max(0, Math.min(100, percent))
},
clear() {
items.length = 0
discountPercent = 0
}
})
</script>
{@render children()} When any item is added, removed, or changed:
itemsarray is mutated- All
$derivedvalues recalculate automatically - Any consumer reading those values re-renders
Consumers Get Automatic Updates
<!-- CartSummary.svelte -->
<script>
import { getContext } from 'svelte'
const cart = getContext('cart')
</script>
<div class="cart-summary">
{#if cart.isEmpty}
<p>Your cart is empty</p>
{:else}
<p>
{cart.itemCount}
{cart.itemCount === 1 ? 'item' : 'items'}
({cart.totalQuantity} total)
</p>
<div class="totals">
<div class="line">
<span>Subtotal</span>
<span>${(cart.subtotal / 100).toFixed(2)}</span>
</div>
{#if cart.discountAmount > 0}
<div class="line discount">
<span>Discount</span>
<span>-${(cart.discountAmount / 100).toFixed(2)}</span>
</div>
{/if}
<div class="line total">
<span>Total</span>
<span>${(cart.total / 100).toFixed(2)}</span>
</div>
</div>
{/if}
</div> The cart summary updates immediately when items change. You don’t have to manually recalculate totals or worry about stale data.
Chained Derivations
Derived values can depend on other derived values. Svelte handles the dependency graph automatically:
// Base state
let items = $state([])
let taxRate = $state(0.08)
let freeShippingThreshold = 5000 // $50
// First level: derived from state
let subtotal = $derived(items.reduce((sum, item) => sum + item.price * item.quantity, 0))
// Second level: derived from derived
let qualifiesForFreeShipping = $derived(subtotal >= freeShippingThreshold)
let shipping = $derived(qualifiesForFreeShipping ? 0 : 599)
// Third level: derived from multiple derived
let tax = $derived(Math.round(subtotal * taxRate))
let total = $derived(subtotal + shipping + tax) When items changes, everything recalculates in the correct order: subtotal first, then qualifiesForFreeShipping and shipping, then tax and total.
Computing Once vs. Every Access
There’s a subtle difference between getters that compute and $derived:
// Getter: computes on every access
get expensive() {
return items.map(i => heavyCalculation(i))
}
// $derived: computes once when dependencies change, then caches
let expensive = $derived(items.map(i => heavyCalculation(i)))
get expensive() { return expensive } For cheap operations, the difference doesn’t matter. For expensive computations, use $derived to avoid redundant work.
Side Effects with $effect
Use $effect inside providers to react to context state changes. This is where you handle persistence, synchronization with external systems, analytics, and DOM updates.
Persisting to localStorage
A common pattern is saving user preferences:
<!-- ThemeProvider.svelte -->
<script>
import { setContext } from 'svelte'
import { browser } from '$app/environment'
let { children } = $props()
// Initialize from localStorage if available
let initialTheme = 'light'
if (browser) {
const stored = localStorage.getItem('theme')
if (stored === 'light' || stored === 'dark') {
initialTheme = stored
}
}
let theme = $state(initialTheme)
// Persist changes to localStorage
$effect(() => {
if (browser) {
localStorage.setItem('theme', theme)
}
})
// Apply to document
$effect(() => {
if (browser) {
document.documentElement.setAttribute('data-theme', theme)
}
})
setContext('theme', {
get current() {
return theme
},
setTheme(value) {
if (value === 'light' || value === 'dark') {
theme = value
}
},
toggle() {
theme = theme === 'light' ? 'dark' : 'light'
}
})
</script>
{@render children()} The $effect runs whenever theme changes, automatically keeping localStorage and the DOM in sync.
Syncing with External Systems
Here’s a pattern for keeping context synchronized with an API:
<!-- NotificationProvider.svelte -->
<script>
import { setContext } from 'svelte'
import { browser } from '$app/environment'
let { children } = $props()
let notifications = $state([])
let unreadCount = $derived(notifications.filter((n) => !n.read).length)
// Poll for new notifications
$effect(() => {
if (!browser) return
async function checkForNew() {
try {
const response = await fetch('/api/notifications')
const data = await response.json()
// Merge new notifications (mutate, don't replace)
for (const notification of data) {
if (!notifications.find((n) => n.id === notification.id)) {
notifications.push(notification)
}
}
} catch (error) {
console.error('Failed to fetch notifications:', error)
}
}
// Check immediately and then every 30 seconds
checkForNew()
const interval = setInterval(checkForNew, 30000)
// Cleanup on unmount
return () => clearInterval(interval)
})
setContext('notifications', {
get all() {
return notifications
},
get unreadCount() {
return unreadCount
},
markAsRead(id) {
const notification = notifications.find((n) => n.id === id)
if (notification) {
notification.read = true
}
},
markAllAsRead() {
for (const notification of notifications) {
notification.read = true
}
},
dismiss(id) {
const index = notifications.findIndex((n) => n.id === id)
if (index !== -1) {
notifications.splice(index, 1)
}
}
})
</script>
{@render children()} Effect Cleanup
When effects set up subscriptions, timers, or event listeners, return a cleanup function:
<script>
import { setContext } from 'svelte'
let { children } = $props()
let networkStatus = $state({ online: navigator.onLine })
$effect(() => {
function handleOnline() {
networkStatus.online = true
}
function handleOffline() {
networkStatus.online = false
}
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
// Cleanup: runs before effect re-runs or component unmounts
return () => {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
})
setContext('network', {
get isOnline() {
return networkStatus.online
},
get isOffline() {
return !networkStatus.online
}
})
</script>
{@render children()} The cleanup function prevents memory leaks and stale event handlers.
Multiple Effects for Separation of Concerns
Don’t put everything in one effect. Separate concerns for clarity:
<script>
import { setContext } from 'svelte'
import { browser } from '$app/environment'
let { children } = $props()
let user = $state(null)
let preferences = $state({ theme: 'light', fontSize: 16 })
// Effect 1: Persist user to sessionStorage
$effect(() => {
if (!browser) return
if (user) {
sessionStorage.setItem('user', JSON.stringify(user))
} else {
sessionStorage.removeItem('user')
}
})
// Effect 2: Persist preferences to localStorage
$effect(() => {
if (!browser) return
localStorage.setItem('preferences', JSON.stringify(preferences))
})
// Effect 3: Apply theme to document
$effect(() => {
if (!browser) return
document.documentElement.setAttribute('data-theme', preferences.theme)
})
// Effect 4: Apply font size to document
$effect(() => {
if (!browser) return
document.documentElement.style.setProperty('--base-font-size', `${preferences.fontSize}px`)
})
// Effect 5: Analytics tracking
$effect(() => {
if (!browser || !user) return
analytics.identify(user.id, {
email: user.email,
name: user.name
})
})
setContext('app', {
get user() {
return user
},
get preferences() {
return preferences
}
// ... methods
})
</script>
{@render children()} Each effect has a single responsibility, making the code easier to understand and maintain.
Context in .svelte.ts Files
For reusable, type-safe context, create modules in .svelte.ts files. This separates context logic from components and enables better organization.
A Complete Counter Module
// lib/context/counter.svelte.ts
import { setContext, getContext, hasContext } from 'svelte'
// Use Symbol for collision-proof keys
const COUNTER_KEY = Symbol('counter')
// Type definitions
export interface CounterContext {
readonly value: number
readonly isAtMin: boolean
readonly isAtMax: boolean
increment(): void
decrement(): void
set(value: number): void
reset(): void
}
export interface CounterOptions {
initial?: number
min?: number
max?: number
step?: number
}
// Creation function
export function createCounterContext(options: CounterOptions = {}): CounterContext {
const { initial = 0, min = -Infinity, max = Infinity, step = 1 } = options
// Internal state
let count = $state(initial)
// Derived values
let isAtMin = $derived(count <= min)
let isAtMax = $derived(count >= max)
// Context API
const context: CounterContext = {
get value() {
return count
},
get isAtMin() {
return isAtMin
},
get isAtMax() {
return isAtMax
},
increment() {
if (count + step <= max) {
count += step
}
},
decrement() {
if (count - step >= min) {
count -= step
}
},
set(value: number) {
count = Math.max(min, Math.min(max, value))
},
reset() {
count = initial
}
}
return setContext(COUNTER_KEY, context)
}
// Getter with error handling
export function getCounterContext(): CounterContext {
if (!hasContext(COUNTER_KEY)) {
throw new Error(
'Counter context not found. ' +
'Wrap your component tree with CounterProvider ' +
'or call createCounterContext().'
)
}
return getContext(COUNTER_KEY)
}
// Optional getter for graceful handling
export function getCounterContextOrNull(): CounterContext | null {
return hasContext(COUNTER_KEY) ? getContext(COUNTER_KEY) : null
}
// Check helper
export function hasCounterContext(): boolean {
return hasContext(COUNTER_KEY)
} Using the Module
Create a provider component:
<!-- CounterProvider.svelte -->
<script lang="ts">
import { createCounterContext, type CounterOptions } from '$lib/context/counter.svelte'
interface Props extends CounterOptions {
children: import('svelte').Snippet
}
let { initial = 0, min, max, step, children }: Props = $props()
createCounterContext({ initial, min, max, step })
</script>
{@render children()} Use in consumers:
<!-- CounterDisplay.svelte -->
<script lang="ts">
import { getCounterContext } from '$lib/context/counter.svelte'
const counter = getCounterContext()
</script>
<div class="counter">
<button onclick={counter.decrement} disabled={counter.isAtMin}> − </button>
<span>{counter.value}</span>
<button onclick={counter.increment} disabled={counter.isAtMax}> + </button>
</div> TypeScript provides full autocomplete and type checking:
counter.value // ✅ number
counter.isAtMin // ✅ boolean
counter.increment() // ✅ void
counter.foo // ❌ Property 'foo' does not exist A More Complex Example: Cart Context
// lib/context/cart.svelte.ts
import { setContext, getContext, hasContext } from 'svelte'
import { browser } from '$app/environment'
const CART_KEY = Symbol('cart')
const STORAGE_KEY = 'shopping_cart'
export interface CartItem {
id: string
name: string
price: number
quantity: number
}
export interface CartContext {
readonly items: CartItem[]
readonly itemCount: number
readonly subtotal: number
readonly isEmpty: boolean
addItem(item: Omit<CartItem, 'quantity'>, quantity?: number): void
removeItem(id: string): void
updateQuantity(id: string, quantity: number): void
clear(): void
}
export function createCartContext(): CartContext {
// Load from storage
function loadItems(): CartItem[] {
if (!browser) return []
try {
const stored = localStorage.getItem(STORAGE_KEY)
return stored ? JSON.parse(stored) : []
} catch {
return []
}
}
// State
let items = $state<CartItem[]>(loadItems())
// Derived
let itemCount = $derived(items.length)
let subtotal = $derived(items.reduce((sum, item) => sum + item.price * item.quantity, 0))
let isEmpty = $derived(items.length === 0)
// Persistence effect
$effect(() => {
if (browser) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(items))
}
})
// API
const context: CartContext = {
get items() {
return items
},
get itemCount() {
return itemCount
},
get subtotal() {
return subtotal
},
get isEmpty() {
return isEmpty
},
addItem(item, quantity = 1) {
const existing = items.find((i) => i.id === item.id)
if (existing) {
existing.quantity += quantity
} else {
items.push({ ...item, quantity })
}
},
removeItem(id) {
const index = items.findIndex((i) => i.id === id)
if (index !== -1) {
items.splice(index, 1)
}
},
updateQuantity(id, quantity) {
const item = items.find((i) => i.id === id)
if (item) {
if (quantity <= 0) {
this.removeItem(id)
} else {
item.quantity = quantity
}
}
},
clear() {
items.length = 0
}
}
return setContext(CART_KEY, context)
}
export function getCartContext(): CartContext {
if (!hasContext(CART_KEY)) {
throw new Error('Cart context not found. Wrap with CartProvider.')
}
return getContext(CART_KEY)
} Organizing Context Modules
A typical structure for a medium-sized app:
src/lib/context/
├── cart.svelte.ts # Shopping cart
├── auth.svelte.ts # Authentication
├── theme.svelte.ts # Theme preferences
├── notifications.svelte.ts # Toast/notification system
├── types.ts # Shared types
└── index.ts # Public exports The index.ts re-exports everything for clean imports:
// lib/context/index.ts
export * from './cart.svelte'
export * from './auth.svelte'
export * from './theme.svelte'
export * from './notifications.svelte' Consumers import what they need:
<script lang="ts">
import { getCartContext, getAuthContext, getThemeContext } from '$lib/context'
const cart = getCartContext()
const auth = getAuthContext()
const theme = getThemeContext()
</script> Putting It All Together
Let’s see these patterns combined in a realistic notification system:
// lib/context/notifications.svelte.ts
import { setContext, getContext, hasContext } from 'svelte'
const NOTIFICATIONS_KEY = Symbol('notifications')
export type NotificationType = 'info' | 'success' | 'warning' | 'error'
export interface Notification {
id: string
message: string
type: NotificationType
dismissible: boolean
createdAt: number
}
export interface NotificationContext {
readonly all: Notification[]
readonly count: number
readonly hasNotifications: boolean
// Convenience methods
info(message: string): string
success(message: string): string
warning(message: string): string
error(message: string): string
// Core methods
add(message: string, options?: NotificationOptions): string
dismiss(id: string): void
dismissAll(): void
}
export interface NotificationOptions {
type?: NotificationType
duration?: number
dismissible?: boolean
}
export function createNotificationContext(): NotificationContext {
let notifications = $state<Notification[]>([])
// Derived
let count = $derived(notifications.length)
let hasNotifications = $derived(count > 0)
function generateId(): string {
return `notification-${Date.now()}-${Math.random().toString(36).slice(2)}`
}
function addNotification(message: string, options: NotificationOptions = {}): string {
const { type = 'info', duration = type === 'error' ? 0 : 5000, dismissible = true } = options
const id = generateId()
notifications.push({
id,
message,
type,
dismissible,
createdAt: Date.now()
})
// Auto-dismiss if duration > 0
if (duration > 0) {
setTimeout(() => dismiss(id), duration)
}
return id
}
function dismiss(id: string): void {
const index = notifications.findIndex((n) => n.id === id)
if (index !== -1) {
notifications.splice(index, 1)
}
}
function dismissAll(): void {
notifications.length = 0
}
const context: NotificationContext = {
get all() {
return notifications
},
get count() {
return count
},
get hasNotifications() {
return hasNotifications
},
info: (message) => addNotification(message, { type: 'info' }),
success: (message) => addNotification(message, { type: 'success' }),
warning: (message) => addNotification(message, { type: 'warning' }),
error: (message) => addNotification(message, { type: 'error' }),
add: addNotification,
dismiss,
dismissAll
}
return setContext(NOTIFICATIONS_KEY, context)
}
export function getNotificationContext(): NotificationContext {
if (!hasContext(NOTIFICATIONS_KEY)) {
throw new Error('Notification context not found. Wrap with NotificationProvider.')
}
return getContext(NOTIFICATIONS_KEY)
} Provider with UI:
<!-- NotificationProvider.svelte -->
<script lang="ts">
import { createNotificationContext } from '$lib/context/notifications.svelte'
import { fly, fade } from 'svelte/transition'
let { children } = $props()
const notifications = createNotificationContext()
</script>
{@render children()}
<!-- Notification toast container -->
{#if notifications.hasNotifications}
<div class="notification-container" role="region" aria-label="Notifications">
{#each notifications.all as notification (notification.id)}
<div
class="notification notification-{notification.type}"
role="alert"
in:fly={{ y: 20, duration: 200 }}
out:fade={{ duration: 150 }}
>
<span class="message">{notification.message}</span>
{#if notification.dismissible}
<button
class="dismiss"
onclick={() => notifications.dismiss(notification.id)}
aria-label="Dismiss"
>
×
</button>
{/if}
</div>
{/each}
</div>
{/if}
<style>
.notification-container {
position: fixed;
bottom: 1rem;
right: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
z-index: 9999;
}
.notification {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem 1rem;
border-radius: 8px;
background: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
max-width: 400px;
}
.notification-success {
border-left: 4px solid #10b981;
}
.notification-error {
border-left: 4px solid #ef4444;
}
.notification-warning {
border-left: 4px solid #f59e0b;
}
.notification-info {
border-left: 4px solid #3b82f6;
}
.dismiss {
padding: 0.25rem;
border: none;
background: none;
font-size: 1.25rem;
cursor: pointer;
opacity: 0.5;
}
.dismiss:hover {
opacity: 1;
}
</style> Usage throughout the app:
<script lang="ts">
import { getNotificationContext } from '$lib/context/notifications.svelte'
const notify = getNotificationContext()
async function handleSave() {
try {
await saveDocument()
notify.success('Document saved successfully!')
} catch (err) {
notify.error(`Failed to save: ${err.message}`)
}
}
</script>
<button onclick={handleSave}>Save Document</button>
{#if notify.hasNotifications}
<span class="badge">{notify.count}</span>
{/if} Quick Reference
| Pattern | When to Use | Example |
|---|---|---|
| Getters | Always for reading state | get value() { return count } |
| Methods | For mutations with validation | increment() { if (canIncrement) count++ } |
| $derived | Computed values from state | let total = $derived(subtotal + tax) |
| $effect | Side effects (persistence, sync) | $effect(() => localStorage.setItem(key, value)) |
| Symbol keys | Collision-proof context keys | const KEY = Symbol('feature') |
| .svelte.ts | Reusable typed context modules | export function createContext() |
Conclusion
Reactive context patterns represent the maturation of state management in Svelte 5. The fundamentals—$state objects and the “mutate, don’t reassign” rule—provide the foundation. The patterns in this article—getters, $derived, $effect, and .svelte.ts modules—transform that foundation into production-quality architecture.
The progression is natural: you start by exposing raw state, then realize you need encapsulation (getters). You find yourself computing the same values repeatedly, then discover $derived handles it automatically. You need persistence or synchronization, and $effect provides the reactive hook. Finally, you want reuse and type safety, and .svelte.ts modules deliver.
What makes these patterns powerful is how they compose. A notification system might use all of them: getters for count and hasNotifications, $derived for filtering by type, $effect for auto-dismissal timers, and a .svelte.ts module for the complete typed API. Each pattern solves one problem well, and together they solve complex problems elegantly.
The investment in learning these patterns pays dividends in every context you build. Your APIs become cleaner, your code more maintainable, and your consumers—whether that’s you next month or a teammate next year—will thank you for the clarity.
Key Takeaways
- Getters ensure current values—always expose reactive state through
get value() { return state }rather than capturing values at creation time - Getters enable encapsulation—internal implementation can change without breaking consumers, as long as the getter interface stays the same
- $derived handles computed values—Svelte automatically tracks dependencies and recalculates when they change, eliminating manual update logic
- Derived values can chain—
subtotaldepends onitems,totaldepends onsubtotalandshipping—Svelte handles the dependency graph - $effect manages side effects—use for persistence, external synchronization, DOM updates, and analytics; return cleanup functions for listeners and timers
- Separate concerns across multiple effects—each effect should have a single responsibility for clarity and maintainability
- .svelte.ts modules enable reuse—create typed context modules with Symbol keys, creation functions, and typed getters for professional APIs
- Export complete APIs—provide creation functions, required and optional getters, and clear TypeScript interfaces
What’s Next
Take encapsulation further with Class-Based Context, where TypeScript classes combine with runes for powerful state management, private fields, and inheritance patterns.
See Also
Official Documentation
- $state — Reactive state
- $derived — Computed values
- $effect — Side effects
- .svelte.ts files — Runes in TypeScript
Related Articles
- Reactive Context Fundamentals — Why context isn’t reactive and how to fix it
- Context for Theme Management — Complete theme system
- Class-Based Context — Advanced TypeScript patterns