Introduction
The $derived rune creates values that are computed from other reactive values. When those source values change, the derived value automatically recalculates. This is one of the most powerful concepts in Svelte 5’s reactivity system—it lets you express complex data relationships declaratively while the framework handles all the update logic.
Analogy: Think of a derived value like a live scoreboard at a sports game. The scoreboard doesn’t have its own opinion about the score—it simply reflects the current state of the game. When a team scores, the scoreboard automatically updates.
Svelte 5 provides two forms of the derived rune:
$derived(expression)— For simple, single-expression computations$derived.by(() => { ... })— For complex logic requiring multiple statements
This tutorial covers each form in depth, explains when to use which, and provides extensive real-world examples to help you master reactive computations in Svelte 5.
Part 1: The $derived Rune
What Is $derived?
$derived creates a reactive value from a single expression. When any reactive value in that expression changes, the derived value automatically recalculates.
Key ConstraintDerived values are automatically memoized—they only recalculate when dependencies change, making them perfect for expensive computations.
<script>
let count = $state(5)
let doubled = $derived(count * 2) // Always count * 2
</script>
<p>{count} × 2 = {doubled}</p> The key constraint: $derived() takes exactly one expression — no semicolons, no multiple statements, no control flow. Think of it as a formula in a spreadsheet cell.
Basic Syntax
let derivedValue = $derived(expression) Where expression can be:
- Arithmetic:
count * 2,price + tax - Method calls:
items.filter(...),name.toUpperCase() - Ternary operators:
isActive ? 'Yes' : 'No' - Template literals:
`Hello, ${name}!` - Property access:
user.profile.name - Chained methods:
items.filter(...).map(...).join(', ')
When to Use $derived
Use $derived when your computation fits in a single expression with no intermediate steps.
Good use cases for $derived:
<script>
let price = $state(100)
let quantity = $state(2)
let taxRate = $state(0.08)
let items = $state(['banana', 'apple', 'cherry'])
let user = $state({ firstName: 'Alice', lastName: 'Smith' })
// Simple arithmetic
let subtotal = $derived(price * quantity)
let tax = $derived(subtotal * taxRate)
let total = $derived(subtotal + tax)
// Array operations
let sorted = $derived([...items].sort())
let count = $derived(items.length)
let first = $derived(items[0])
let hasItems = $derived(items.length > 0)
// String operations
let fullName = $derived(`${user.firstName} ${user.lastName}`)
let initials = $derived(`${user.firstName[0]}${user.lastName[0]}`)
// Ternary expressions
let status = $derived(total > 500 ? 'Premium Order' : 'Standard Order')
let discount = $derived(quantity >= 10 ? 0.1 : 0)
// Chained methods (still one expression)
let processed = $derived(
items
.filter((i) => i.length > 4)
.map((i) => i.toUpperCase())
.join(', ')
)
// Nullish coalescing and optional chaining
let displayName = $derived(user?.nickname ?? user.firstName)
</script> Rule of thumb: If you can write it without semicolons or curly braces { }, use $derived().
Real-World Examples with $derived
1. Shopping Cart Totals
<script>
let cart = $state([
{ name: 'T-Shirt', price: 25.99, quantity: 2 },
{ name: 'Jeans', price: 59.99, quantity: 1 },
{ name: 'Socks', price: 8.99, quantity: 3 }
])
let itemCount = $derived(cart.reduce((sum, item) => sum + item.quantity, 0))
let subtotal = $derived(cart.reduce((sum, item) => sum + item.price * item.quantity, 0))
let shipping = $derived(subtotal > 100 ? 0 : 9.99)
let total = $derived(subtotal + shipping)
let freeShippingRemaining = $derived(Math.max(0, 100 - subtotal))
</script>
<h2>Your Cart ({itemCount} items)</h2>
{#each cart as item}
<div class="cart-item">
<span>{item.name}</span>
<input type="number" bind:value={item.quantity} min="1" />
<span>${(item.price * item.quantity).toFixed(2)}</span>
</div>
{/each}
<hr />
<p>Subtotal: ${subtotal.toFixed(2)}</p>
<p>Shipping: {shipping === 0 ? 'FREE' : `$${shipping.toFixed(2)}`}</p>
{#if freeShippingRemaining > 0}
<p class="promo">Add ${freeShippingRemaining.toFixed(2)} more for free shipping!</p>
{/if}
<p class="total">Total: ${total.toFixed(2)}</p> 2. Search and Filter
<script>
let products = $state([
{ name: 'MacBook Pro', category: 'Electronics', price: 1999 },
{ name: 'iPhone', category: 'Electronics', price: 999 },
{ name: 'Coffee Maker', category: 'Kitchen', price: 79 },
{ name: 'Standing Desk', category: 'Furniture', price: 599 },
{ name: 'Headphones', category: 'Electronics', price: 299 }
])
let searchQuery = $state('')
let selectedCategory = $state('all')
let maxPrice = $state(2000)
// Each filter step is a simple expression
let searchFiltered = $derived(
products.filter((p) => p.name.toLowerCase().includes(searchQuery.toLowerCase()))
)
let categoryFiltered = $derived(
selectedCategory === 'all'
? searchFiltered
: searchFiltered.filter((p) => p.category === selectedCategory)
)
let priceFiltered = $derived(categoryFiltered.filter((p) => p.price <= maxPrice))
let resultCount = $derived(priceFiltered.length)
let categories = $derived([...new Set(products.map((p) => p.category))])
</script>
<input type="search" bind:value={searchQuery} placeholder="Search products..." />
<select bind:value={selectedCategory}>
<option value="all">All Categories</option>
{#each categories as cat}
<option value={cat}>{cat}</option>
{/each}
</select>
<label>
Max Price: ${maxPrice}
<input type="range" bind:value={maxPrice} min="0" max="2000" />
</label>
<p>Showing {resultCount} products</p>
{#each priceFiltered as product}
<div class="product">
<h3>{product.name}</h3>
<p>{product.category} • ${product.price}</p>
</div>
{/each} 3. Real-Time Character Counter
<script>
let text = $state('')
let maxLength = $state(280)
let charCount = $derived(text.length)
let remaining = $derived(maxLength - charCount)
let percentage = $derived((charCount / maxLength) * 100)
let isOverLimit = $derived(charCount > maxLength)
let isNearLimit = $derived(remaining <= 20 && remaining > 0)
let wordCount = $derived(text.trim() ? text.trim().split(/\s+/).length : 0)
</script>
<textarea bind:value={text} placeholder="What's happening?"></textarea>
<div class="counter" class:warning={isNearLimit} class:error={isOverLimit}>
{remaining} characters remaining • {wordCount} words
</div>
<div class="progress-bar">
<div
class="progress"
style="width: {Math.min(percentage, 100)}%"
class:warning={isNearLimit}
class:error={isOverLimit}
></div>
</div>
<button disabled={isOverLimit || charCount === 0}> Post </button>
<style>
.warning {
color: #f39c12;
}
.error {
color: #e74c3c;
}
.progress-bar {
height: 4px;
background: #eee;
border-radius: 2px;
}
.progress {
height: 100%;
background: #3498db;
transition: width 0.2s;
}
.progress.warning {
background: #f39c12;
}
.progress.error {
background: #e74c3c;
}
</style> 4. Currency Converter
<script>
let amount = $state(100)
let fromCurrency = $state('USD')
let toCurrency = $state('EUR')
// In real app, these would come from an API
let rates = $state({
USD: 1,
EUR: 0.92,
GBP: 0.79,
JPY: 149.5,
CAD: 1.36
})
let converted = $derived((amount / rates[fromCurrency]) * rates[toCurrency])
let formattedResult = $derived(
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: toCurrency
}).format(converted)
)
let exchangeRate = $derived((rates[toCurrency] / rates[fromCurrency]).toFixed(4))
</script>
<input type="number" bind:value={amount} />
<select bind:value={fromCurrency}>
{#each Object.keys(rates) as currency}
<option value={currency}>{currency}</option>
{/each}
</select>
<span>→</span>
<select bind:value={toCurrency}>
{#each Object.keys(rates) as currency}
<option value={currency}>{currency}</option>
{/each}
</select>
<p class="result">{formattedResult}</p>
<p class="rate">1 {fromCurrency} = {exchangeRate} {toCurrency}</p> 5. Date and Time Formatting
<script>
let selectedDate = $state(new Date())
let formattedDate = $derived(selectedDate.toLocaleDateString('en-US', { dateStyle: 'full' }))
let formattedTime = $derived(selectedDate.toLocaleTimeString('en-US', { timeStyle: 'short' }))
let relativeTime = $derived(getRelativeTime(selectedDate))
let dayOfYear = $derived(
Math.floor((selectedDate - new Date(selectedDate.getFullYear(), 0, 0)) / (1000 * 60 * 60 * 24))
)
let weekNumber = $derived(Math.ceil(dayOfYear / 7))
let isWeekend = $derived([0, 6].includes(selectedDate.getDay()))
let quarter = $derived(Math.ceil((selectedDate.getMonth() + 1) / 3))
function getRelativeTime(date) {
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
const diff = date - new Date()
const days = Math.round(diff / (1000 * 60 * 60 * 24))
if (Math.abs(days) < 1) return 'today'
return rtf.format(days, 'day')
}
</script>
<input type="datetime-local" bind:value={selectedDate} />
<dl>
<dt>Full Date</dt>
<dd>{formattedDate}</dd>
<dt>Time</dt>
<dd>{formattedTime}</dd>
<dt>Relative</dt>
<dd>{relativeTime}</dd>
<dt>Day of Year</dt>
<dd>{dayOfYear}</dd>
<dt>Week Number</dt>
<dd>{weekNumber}</dd>
<dt>Quarter</dt>
<dd>Q{quarter}</dd>
<dt>Weekend?</dt>
<dd>{isWeekend ? 'Yes 🎉' : 'No'}</dd>
</dl> Derived Values vs. Direct Template Calculations
Why use $derived instead of calculating directly in the template?
<script>
let items = $state([
{ name: 'Apple', price: 1.5, quantity: 3 },
{ name: 'Banana', price: 0.75, quantity: 5 }
])
// With $derived - calculated once, reused everywhere
let total = $derived(items.reduce((sum, item) => sum + item.price * item.quantity, 0))
</script>
<!-- Option 1: Using $derived (better) -->
<p>Total: ${total.toFixed(2)}</p>
<p>With tax: ${(total * 1.08).toFixed(2)}</p>
<p>Per item average: ${(total / items.length).toFixed(2)}</p>
<!-- Option 2: Calculating in template (worse) -->
<p>Total: ${items.reduce((s, i) => s + i.price * i.quantity, 0).toFixed(2)}</p>
<p>With tax: ${(items.reduce((s, i) => s + i.price * i.quantity, 0) * 1.08).toFixed(2)}</p>
<!-- The reduce runs THREE times! --> Benefits of $derived:
- Calculates once per change, caches the result
- Makes code more readable
- Can be reused across the template
- Easier to debug and test
Part 2: The $derived.by Rune
What Is $derived.by?
$derived.by creates a reactive value from a function body. Unlike $derived, it can contain multiple statements, intermediate variables, control flow, loops, try/catch blocks, and early returns.
<script>
let numbers = $state([1, 5, 3, 9, 2, 7])
let stats = $derived.by(() => {
// Multiple statements and variables
const sorted = [...numbers].sort((a, b) => a - b)
const sum = numbers.reduce((a, b) => a + b, 0)
const avg = sum / numbers.length
// Multi-step calculation
const mid = Math.floor(sorted.length / 2)
const median = sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]
// Return the computed result
return { min: sorted[0], max: sorted.at(-1), sum, avg, median }
})
</script> Basic Syntax
let derivedValue = $derived.by(() => {
// Multiple statements
// Local variables
// Control flow
return result
}) The function passed to $derived.by must:
- Be synchronous (no
async/await) - Return a value
- Be a pure computation (no side effects)
When to Use $derived.by
Use $derived.by when you need:
- Multiple statements — More than one line of logic
- Intermediate variables — Variables for storing partial results
- Control flow —
if/else,switch,for,while - Early returns — Return from different code paths
- Try/catch — Error handling
- Complex transformations — Multi-step data processing
Real-World Examples with $derived.by
1. User Permissions System
<script>
let user = $state({
id: 1,
name: 'Alice',
role: 'editor',
subscription: 'premium',
verified: true,
loginCount: 42,
createdAt: new Date('2023-01-15')
})
let permissions = $derived.by(() => {
// Admin gets everything - early return
if (user.role === 'admin') {
return {
canRead: true,
canWrite: true,
canDelete: true,
canPublish: true,
canManageUsers: true,
canAccessAnalytics: true,
canExport: true
}
}
// Start with base permissions
const perms = {
canRead: true,
canWrite: false,
canDelete: false,
canPublish: false,
canManageUsers: false,
canAccessAnalytics: false,
canExport: false
}
// Role-based permissions
if (user.role === 'editor') {
perms.canWrite = true
perms.canPublish = true
} else if (user.role === 'author') {
perms.canWrite = true
}
// Subscription-based upgrades
if (user.subscription === 'premium') {
perms.canAccessAnalytics = true
perms.canExport = true
} else if (user.subscription === 'pro') {
perms.canExport = true
}
// Verified users can delete their own content
if (user.verified) {
perms.canDelete = true
}
// Veteran users (1+ year) get analytics
const accountAge = Date.now() - user.createdAt.getTime()
const oneYear = 365 * 24 * 60 * 60 * 1000
if (accountAge > oneYear) {
perms.canAccessAnalytics = true
}
return perms
})
</script>
<h2>Permissions for {user.name}</h2>
<ul>
{#each Object.entries(permissions) as [perm, allowed]}
<li class:granted={allowed} class:denied={!allowed}>
{perm}: {allowed ? 'yes' : 'no'}
</li>
{/each}
</ul> 2. Form Validation with Detailed Errors
<script>
let form = $state({
email: '',
password: '',
confirmPassword: '',
age: '',
website: ''
})
let validation = $derived.by(() => {
const errors = {}
const warnings = {}
// Email validation
if (!form.email) {
errors.email = 'Email is required'
} else if (!form.email.includes('@')) {
errors.email = 'Please enter a valid email address'
} else if (form.email.endsWith('.con')) {
warnings.email = 'Did you mean .com?'
}
// Password validation
if (!form.password) {
errors.password = 'Password is required'
} else {
const pwd = form.password
const issues = []
if (pwd.length < 8) issues.push('at least 8 characters')
if (!/[A-Z]/.test(pwd)) issues.push('one uppercase letter')
if (!/[a-z]/.test(pwd)) issues.push('one lowercase letter')
if (!/[0-9]/.test(pwd)) issues.push('one number')
if (!/[^A-Za-z0-9]/.test(pwd)) issues.push('one special character')
if (issues.length > 0) {
errors.password = `Password needs: ${issues.join(', ')}`
}
}
// Confirm password
if (form.password && form.confirmPassword !== form.password) {
errors.confirmPassword = 'Passwords do not match'
}
// Age validation
if (form.age) {
const age = parseInt(form.age)
if (isNaN(age)) {
errors.age = 'Please enter a valid number'
} else if (age < 13) {
errors.age = 'You must be at least 13 years old'
} else if (age > 120) {
errors.age = 'Please enter a realistic age'
} else if (age < 18) {
warnings.age = 'Parental consent may be required'
}
}
// Website validation (optional field)
if (form.website) {
try {
new URL(form.website)
} catch {
// Try adding https://
try {
new URL(`https://${form.website}`)
warnings.website = 'Consider adding https://'
} catch {
errors.website = 'Please enter a valid URL'
}
}
}
const errorCount = Object.keys(errors).length
const warningCount = Object.keys(warnings).length
const isValid = errorCount === 0
return { errors, warnings, errorCount, warningCount, isValid }
})
</script>
<form>
<div class="field">
<label>Email</label>
<input type="email" bind:value={form.email} />
{#if validation.errors.email}
<span class="error">{validation.errors.email}</span>
{:else if validation.warnings.email}
<span class="warning">{validation.warnings.email}</span>
{/if}
</div>
<div class="field">
<label>Password</label>
<input type="password" bind:value={form.password} />
{#if validation.errors.password}
<span class="error">{validation.errors.password}</span>
{/if}
</div>
<div class="field">
<label>Confirm Password</label>
<input type="password" bind:value={form.confirmPassword} />
{#if validation.errors.confirmPassword}
<span class="error">{validation.errors.confirmPassword}</span>
{/if}
</div>
<div class="field">
<label>Age</label>
<input type="number" bind:value={form.age} />
{#if validation.errors.age}
<span class="error">{validation.errors.age}</span>
{:else if validation.warnings.age}
<span class="warning">{validation.warnings.age}</span>
{/if}
</div>
<div class="field">
<label>Website (optional)</label>
<input type="text" bind:value={form.website} />
{#if validation.errors.website}
<span class="error">{validation.errors.website}</span>
{:else if validation.warnings.website}
<span class="warning">{validation.warnings.website}</span>
{/if}
</div>
<button type="submit" disabled={!validation.isValid}>
Submit ({validation.errorCount} errors, {validation.warningCount} warnings)
</button>
</form> 3. Financial Report Generator
<script>
let transactions = $state([
{ date: '2024-01-05', type: 'income', amount: 5000, category: 'salary' },
{ date: '2024-01-10', type: 'expense', amount: 1200, category: 'rent' },
{ date: '2024-01-12', type: 'expense', amount: 85, category: 'utilities' },
{ date: '2024-01-15', type: 'income', amount: 500, category: 'freelance' },
{ date: '2024-01-18', type: 'expense', amount: 150, category: 'groceries' },
{ date: '2024-01-22', type: 'expense', amount: 60, category: 'entertainment' },
{ date: '2024-01-25', type: 'expense', amount: 200, category: 'groceries' },
{ date: '2024-01-28', type: 'income', amount: 250, category: 'dividends' }
])
let report = $derived.by(() => {
// Initialize accumulators
let totalIncome = 0
let totalExpenses = 0
const incomeByCategory = {}
const expensesByCategory = {}
const dailyBalance = []
// Track running balance for chart data
let runningBalance = 0
// Sort transactions by date for running balance
const sortedTransactions = [...transactions].sort((a, b) => new Date(a.date) - new Date(b.date))
// Single pass through data
for (const t of sortedTransactions) {
if (t.type === 'income') {
totalIncome += t.amount
incomeByCategory[t.category] = (incomeByCategory[t.category] || 0) + t.amount
runningBalance += t.amount
} else {
totalExpenses += t.amount
expensesByCategory[t.category] = (expensesByCategory[t.category] || 0) + t.amount
runningBalance -= t.amount
}
dailyBalance.push({
date: t.date,
balance: runningBalance,
transaction: t
})
}
// Calculate derived metrics
const netIncome = totalIncome - totalExpenses
const savingsRate = totalIncome > 0 ? ((netIncome / totalIncome) * 100).toFixed(1) : 0
// Find largest expense category
const largestExpenseCategory = Object.entries(expensesByCategory).sort(
([, a], [, b]) => b - a
)[0]
// Monthly comparison (if we had previous month data)
const expenseBreakdown = Object.entries(expensesByCategory)
.map(([category, amount]) => ({
category,
amount,
percentage: ((amount / totalExpenses) * 100).toFixed(1)
}))
.sort((a, b) => b.amount - a.amount)
// Budget warnings
const warnings = []
if (savingsRate < 10) {
warnings.push('Savings rate below recommended 10%')
}
if (expensesByCategory.entertainment > totalIncome * 0.1) {
warnings.push('Entertainment exceeds 10% of income')
}
return {
totalIncome,
totalExpenses,
netIncome,
savingsRate,
incomeByCategory,
expensesByCategory,
expenseBreakdown,
largestExpenseCategory: largestExpenseCategory
? { name: largestExpenseCategory[0], amount: largestExpenseCategory[1] }
: null,
dailyBalance,
currentBalance: runningBalance,
transactionCount: transactions.length,
warnings
}
})
</script>
<h2>Financial Report</h2>
<div class="summary-cards">
<div class="card income">
<h3>Income</h3>
<p class="amount">${report.totalIncome.toLocaleString()}</p>
</div>
<div class="card expenses">
<h3>Expenses</h3>
<p class="amount">${report.totalExpenses.toLocaleString()}</p>
</div>
<div
class="card balance"
class:positive={report.netIncome >= 0}
class:negative={report.netIncome < 0}
>
<h3>Net</h3>
<p class="amount">${report.netIncome.toLocaleString()}</p>
</div>
</div>
<p>Savings Rate: {report.savingsRate}%</p>
{#if report.largestExpenseCategory}
<p>
Largest expense: {report.largestExpenseCategory.name} (${report.largestExpenseCategory.amount})
</p>
{/if}
<h3>Expense Breakdown</h3>
{#each report.expenseBreakdown as item}
<div class="expense-bar">
<span>{item.category}</span>
<div class="bar" style="width: {item.percentage}%"></div>
<span>${item.amount} ({item.percentage}%)</span>
</div>
{/each}
{#if report.warnings.length > 0}
<div class="warnings">
<h3>Warnings</h3>
<ul>
{#each report.warnings as warning}
<li>{warning}</li>
{/each}
</ul>
</div>
{/if} 4. Markdown Parser with Error Handling
<script>
let markdownInput = $state(`# Hello World
This is a **bold** statement and this is *italic*.
- Item 1
- Item 2
- Item 3
> A blockquote
\`\`\`javascript
const x = 42;
\`\`\`
`)
let parsed = $derived.by(() => {
try {
let html = markdownInput
// Track what we've processed for stats
const stats = {
headings: 0,
bold: 0,
italic: 0,
lists: 0,
codeBlocks: 0,
blockquotes: 0
}
// Process code blocks first (to avoid processing their contents)
const codeBlocks = []
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => {
stats.codeBlocks++
const placeholder = `__CODE_BLOCK_${codeBlocks.length}__`
codeBlocks.push({ lang, code: code.trim() })
return placeholder
})
// Headings
html = html.replace(/^### (.+)$/gm, (_, text) => {
stats.headings++
return `<h3>${text}</h3>`
})
html = html.replace(/^## (.+)$/gm, (_, text) => {
stats.headings++
return `<h2>${text}</h2>`
})
html = html.replace(/^# (.+)$/gm, (_, text) => {
stats.headings++
return `<h1>${text}</h1>`
})
// Bold and italic
html = html.replace(/\*\*(.+?)\*\*/g, (_, text) => {
stats.bold++
return `<strong>${text}</strong>`
})
html = html.replace(/\*(.+?)\*/g, (_, text) => {
stats.italic++
return `<em>${text}</em>`
})
// Lists
html = html.replace(/^- (.+)$/gm, (_, text) => {
stats.lists++
return `<li>${text}</li>`
})
html = html.replace(/(<li>.*<\/li>\n?)+/g, (match) => {
return `<ul>${match}</ul>`
})
// Blockquotes
html = html.replace(/^> (.+)$/gm, (_, text) => {
stats.blockquotes++
return `<blockquote>${text}</blockquote>`
})
// Restore code blocks
codeBlocks.forEach((block, i) => {
const highlighted = `<pre><code class="language-${block.lang}">${escapeHtml(block.code)}</code></pre>`
html = html.replace(`__CODE_BLOCK_${i}__`, highlighted)
})
// Paragraphs (lines that aren't already wrapped)
html = html
.split('\n\n')
.map((block) => {
if (block.match(/^<[^>]+>/)) return block
if (block.trim()) return `<p>${block}</p>`
return ''
})
.join('\n')
return {
success: true,
html,
stats,
error: null
}
} catch (error) {
return {
success: false,
html: '',
stats: null,
error: error.message
}
}
})
function escapeHtml(text) {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
}
</script>
<div class="editor">
<textarea bind:value={markdownInput}></textarea>
<div class="preview">
{#if parsed.success}
{@html parsed.html}
<div class="stats">
<small>
{parsed.stats.headings} headings •
{parsed.stats.bold} bold •
{parsed.stats.italic} italic •
{parsed.stats.codeBlocks} code blocks
</small>
</div>
{:else}
<div class="error">
Parse error: {parsed.error}
</div>
{/if}
</div>
</div> 5. Smart Data Grouping and Aggregation
<script>
let employees = $state([
{ id: 1, name: 'Alice', department: 'Engineering', salary: 95000, startDate: '2020-03-15' },
{ id: 2, name: 'Bob', department: 'Marketing', salary: 72000, startDate: '2021-06-01' },
{ id: 3, name: 'Carol', department: 'Engineering', salary: 88000, startDate: '2019-11-20' },
{ id: 4, name: 'David', department: 'Sales', salary: 68000, startDate: '2022-01-10' },
{ id: 5, name: 'Eve', department: 'Engineering', salary: 102000, startDate: '2018-07-22' },
{ id: 6, name: 'Frank', department: 'Marketing', salary: 78000, startDate: '2020-09-05' },
{ id: 7, name: 'Grace', department: 'Sales', salary: 71000, startDate: '2021-03-18' }
])
let groupBy = $state('department')
let analysis = $derived.by(() => {
const groups = {}
const companyStats = {
totalEmployees: employees.length,
totalSalary: 0,
avgSalary: 0,
seniorityDistribution: { '<1yr': 0, '1-2yr': 0, '2-3yr': 0, '3+yr': 0 }
}
const now = new Date()
// First pass: group employees and gather company-wide stats
for (const emp of employees) {
// Group by selected field
const groupKey = emp[groupBy]
if (!groups[groupKey]) {
groups[groupKey] = {
employees: [],
totalSalary: 0,
count: 0
}
}
groups[groupKey].employees.push(emp)
groups[groupKey].totalSalary += emp.salary
groups[groupKey].count++
// Company-wide stats
companyStats.totalSalary += emp.salary
// Seniority calculation
const startDate = new Date(emp.startDate)
const yearsEmployed = (now - startDate) / (365.25 * 24 * 60 * 60 * 1000)
if (yearsEmployed < 1) companyStats.seniorityDistribution['<1yr']++
else if (yearsEmployed < 2) companyStats.seniorityDistribution['1-2yr']++
else if (yearsEmployed < 3) companyStats.seniorityDistribution['2-3yr']++
else companyStats.seniorityDistribution['3+yr']++
}
companyStats.avgSalary = companyStats.totalSalary / companyStats.totalEmployees
// Second pass: calculate per-group statistics
const groupStats = []
for (const [name, data] of Object.entries(groups)) {
const salaries = data.employees.map((e) => e.salary).sort((a, b) => a - b)
const avgSalary = data.totalSalary / data.count
// Find newest and most senior employee
const sorted = [...data.employees].sort(
(a, b) => new Date(a.startDate) - new Date(b.startDate)
)
groupStats.push({
name,
count: data.count,
totalSalary: data.totalSalary,
avgSalary,
minSalary: salaries[0],
maxSalary: salaries[salaries.length - 1],
salaryRange: salaries[salaries.length - 1] - salaries[0],
topEarner: data.employees.reduce((top, emp) => (emp.salary > top.salary ? emp : top)),
mostSenior: sorted[0],
newest: sorted[sorted.length - 1],
percentOfCompany: ((data.count / companyStats.totalEmployees) * 100).toFixed(1)
})
}
// Sort groups by count (largest first)
groupStats.sort((a, b) => b.count - a.count)
return {
groups: groupStats,
company: companyStats,
groupCount: groupStats.length
}
})
</script>
<h2>Employee Analysis</h2>
<label>
Group by:
<select bind:value={groupBy}>
<option value="department">Department</option>
</select>
</label>
<div class="company-overview">
<h3>Company Overview</h3>
<p>Total Employees: {analysis.company.totalEmployees}</p>
<p>Total Payroll: ${analysis.company.totalSalary.toLocaleString()}</p>
<p>Average Salary: ${analysis.company.avgSalary.toLocaleString()}</p>
</div>
<div class="seniority">
<h4>Seniority Distribution</h4>
{#each Object.entries(analysis.company.seniorityDistribution) as [range, count]}
<span>{range}: {count}</span>
{/each}
</div>
<h3>By {groupBy} ({analysis.groupCount} groups)</h3>
{#each analysis.groups as group}
<details open>
<summary>
{group.name} - {group.count} employees ({group.percentOfCompany}% of company)
</summary>
<div class="group-stats">
<p>Salary range: ${group.minSalary.toLocaleString()} - ${group.maxSalary.toLocaleString()}</p>
<p>Average: ${Math.round(group.avgSalary).toLocaleString()}</p>
<p>Top earner: {group.topEarner.name} (${group.topEarner.salary.toLocaleString()})</p>
<p>Most senior: {group.mostSenior.name} (since {group.mostSenior.startDate})</p>
<p>Newest: {group.newest.name} (since {group.newest.startDate})</p>
</div>
</details>
{/each} 6. Status Display with Switch
<script>
let orderStatus = $state('processing')
let statusDisplay = $derived.by(() => {
switch (orderStatus) {
case 'pending':
return {
icon: '⏳',
label: 'Pending',
color: '#f39c12',
description: 'Your order is waiting to be processed',
actions: ['Cancel Order']
}
case 'processing':
return {
icon: '⚙️',
label: 'Processing',
color: '#3498db',
description: 'We are preparing your order',
actions: ['Contact Support']
}
case 'shipped':
return {
icon: '📦',
label: 'Shipped',
color: '#9b59b6',
description: 'Your order is on its way',
actions: ['Track Package', 'Contact Support']
}
case 'delivered':
return {
icon: '✅',
label: 'Delivered',
color: '#27ae60',
description: 'Your order has been delivered',
actions: ['Leave Review', 'Return Item', 'Buy Again']
}
case 'cancelled':
return {
icon: '❌',
label: 'Cancelled',
color: '#e74c3c',
description: 'This order was cancelled',
actions: ['View Refund Status', 'Reorder']
}
default:
return {
icon: '❓',
label: 'Unknown',
color: '#95a5a6',
description: 'Status unknown',
actions: ['Contact Support']
}
}
})
</script>
<div class="status-card" style="border-color: {statusDisplay.color}">
<div class="status-header" style="background: {statusDisplay.color}">
<span class="icon">{statusDisplay.icon}</span>
<span class="label">{statusDisplay.label}</span>
</div>
<p>{statusDisplay.description}</p>
<div class="actions">
{#each statusDisplay.actions as action}
<button>{action}</button>
{/each}
</div>
</div>
<label>
Test different statuses:
<select bind:value={orderStatus}>
<option value="pending">Pending</option>
<option value="processing">Processing</option>
<option value="shipped">Shipped</option>
<option value="delivered">Delivered</option>
<option value="cancelled">Cancelled</option>
</select>
</label> Common Mistakes with $derived.by
1. Using $derived.by When $derived Would Suffice
<script>
let count = $state(0)
// AVOID: Overly verbose - don't do this
let doubled = $derived.by(() => {
return count * 2
})
// PREFERRED: Simple and clean
let doubled = $derived(count * 2)
</script> 2. Side Effects in $derived.by
Derived Values Must Be PureNever put side effects inside
$derivedor$derived.by— nofetch, noconsole.log, no mutations to state you don’t own. Derived values are recomputed whenever Svelte decides to, so side effects will fire unpredictably. Move them to$effect.
Derived values should be pure computations. Don’t put side effects inside them:
<script>
let count = $state(0)
// AVOID: Side effects in derived
let doubled = $derived.by(() => {
console.log('Calculating doubled...') // Side effect!
fetch('/api/log', { method: 'POST' }) // Side effect!
someGlobalVariable = count // Side effect!
return count * 2
})
// PREFERRED: Pure computation only
let doubled = $derived(count * 2)
// Put side effects in $effect instead
$effect(() => {
console.log('Count changed to:', count)
})
</script> 3. Async Operations in $derived.by
$derived.by Cannot Be Async
$derived.bymust return a value synchronously. You cannotawaitinside it. For async data, use$state+$effectwith anAbortControllerto handle stale responses.
$derived.by must be synchronous. It cannot be async:
<script>
let userId = $state(1)
// AVOID: This won't work - can't use async/await
let user = $derived.by(async () => {
const response = await fetch(`/api/users/${userId}`)
return response.json()
})
// PREFERRED: For async data, use $effect and $state together
let user = $state(null)
let loading = $state(false)
$effect(() => {
loading = true
fetch(`/api/users/${userId}`)
.then((r) => r.json())
.then((data) => {
user = data
loading = false
})
})
</script> 4. Forgetting the Return Statement
<script>
let items = $state([1, 2, 3])
// AVOID: Missing return - result is undefined!
let sum = $derived.by(() => {
let total = 0
for (const item of items) {
total += item
}
// Oops! Forgot to return
})
// PREFERRED: Always return your computed value
let sum = $derived.by(() => {
let total = 0
for (const item of items) {
total += item
}
return total
})
</script> Part 3: $derived vs $derived.by — The Differences Explained
Now that you’ve seen both forms in action, let’s clarify exactly when to use each one.
The Technical Relationship
Under the hood, $derived(expression) is syntactic sugar for $derived.by(() => expression). The compiler transforms:
let doubled = $derived(count * 2) Into something conceptually like:
let doubled = $derived.by(() => count * 2) They’re the same at runtime—the difference is purely in what syntax is allowed.
Side-by-Side Comparison
| Feature | $derived(expression) | $derived.by(() => { ... }) |
|---|---|---|
| Input | Single expression | Function body |
| Statements | One only | Multiple allowed |
| Local variables | ❌ Not possible | ✅ Allowed |
| Control flow | Only ternary ? : | if/else, switch, loops |
| Early returns | ❌ Not possible | ✅ Allowed |
| Try/catch | ❌ Not possible | ✅ Allowed |
| Readability | Concise, obvious | More verbose |
| Use case | Simple formulas | Complex logic |
| Intent signal | “This is straightforward” | “This has complexity” |
Decision Flowchart
Start
│
▼
Can you write it as a single expression
(no semicolons, no { } curly braces)?
│
├── YES → Use $derived(expression)
│
└── NO → Continue...
│
▼
Do you need any of these?
• Multiple statements
• Intermediate variables
• if/else or switch
• for/while loops
• try/catch
• Early returns
│
├── YES → Use $derived.by(() => { ... })
│
└── NO → You probably can use $derived()
Just write it as one expression Examples: Same Result, Different Approaches
Sometimes you can achieve the same result with either form. Choose based on readability:
<script>
let numbers = $state([1, 2, 3, 4, 5])
// Both work - choose the clearer one
// Simple enough for $derived
let sum = $derived(numbers.reduce((a, b) => a + b, 0))
let avg = $derived(sum / numbers.length)
// Or use $derived.by for more clarity
let stats = $derived.by(() => {
const sum = numbers.reduce((a, b) => a + b, 0)
const avg = sum / numbers.length
return { sum, avg }
})
</script> <script>
let user = $state({ role: 'admin' })
// Ternary works for simple conditions
let isAdmin = $derived(user.role === 'admin' ? true : false)
// Even simpler:
let isAdmin = $derived(user.role === 'admin')
// But complex conditions need $derived.by
let accessLevel = $derived.by(() => {
if (user.role === 'admin') return 'full'
if (user.role === 'editor') return 'write'
if (user.role === 'viewer') return 'read'
return 'none'
})
</script> When $derived Gets Awkward
If you find yourself writing complex expressions like this, switch to $derived.by:
<script>
let cart = $state([])
// AVOID: This works but is hard to read
let summary = $derived(
cart.length === 0
? { empty: true, total: 0, message: 'Cart is empty' }
: {
empty: false,
total: cart.reduce((sum, item) => sum + item.price * item.quantity, 0),
message: `${cart.length} items in cart`
}
)
// PREFERRED: Much clearer with $derived.by
let summary = $derived.by(() => {
if (cart.length === 0) {
return { empty: true, total: 0, message: 'Cart is empty' }
}
const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0)
return {
empty: false,
total,
message: `${cart.length} items in cart`
}
})
</script> Performance Considerations
Both forms have identical performance. The choice is purely about code clarity and maintainability. Svelte’s compiler handles both the same way.
However, keep the computation itself efficient:
<script>
let hugeArray = $state([...Array(100000).keys()])
// AVOID: This runs every time hugeArray changes
let sorted = $derived([...hugeArray].sort())
// Consider if you really need this to be reactive,
// or if you can compute it once or less frequently
</script> Advanced Topics
How Dependency Tracking Works
Svelte tracks dependencies at runtime, not by analyzing your code. It observes which reactive values are actually read during execution.
<script>
let showDetails = $state(false)
let user = $state({
name: 'Alice',
email: 'alice@example.com',
bio: 'Software developer'
})
let display = $derived(showDetails ? `${user.name} - ${user.email}: ${user.bio}` : user.name)
</script> When showDetails is false:
- The
falsebranch executes:user.name - Dependencies:
showDetails,user.name - Changing
user.emailoruser.biodoes NOT recalculatedisplay!
When showDetails is true:
- The
truebranch executes:${user.name} - ${user.email}: ${user.bio} - Dependencies:
showDetails,user.name,user.email,user.bio - Now changing any of these recalculates
display
Why this matters: Svelte is smart about avoiding unnecessary work. It only recalculates when values that were actually used have changed.
Excluding Dependencies with untrack
Sometimes you want to read a reactive value without creating a dependency. The untrack function from 'svelte' lets you do this:
<script>
import { untrack } from 'svelte'
let count = $state(0)
let multiplier = $state(2)
// This derived value depends ONLY on count, not multiplier
let result = $derived.by(() => {
const mult = untrack(() => multiplier) // Read without tracking
return count * mult
})
// Changing count → recalculates result
// Changing multiplier → does NOT recalculate result
</script> Use cases for untrack:
- Configuration values that rarely change
- Logging or debugging without creating dependencies
- One-time reads of reactive state
- Breaking circular dependencies
Automatic Memoization
Derived values only recalculate when their dependencies change:
<script>
let items = $state([
/* ... */
])
let filter = $state('all')
let filtered = $derived.by(() => {
console.log('Recalculating...') // Only logs when items or filter changes
if (filter === 'all') return items
return items.filter((item) => item.category === filter)
})
</script> If you read filtered multiple times in your template, the calculation only runs once per change.
Derived Values Are NOT Proxied
Unlike $state, $derived doesn’t wrap objects in Proxies. The returned value is as-is:
<script>
let items = $state([
{ name: 'A', selected: false },
{ name: 'B', selected: true },
{ name: 'C', selected: false }
])
// This finds the selected item
let selected = $derived(items.find((item) => item.selected))
</script>
<!--
selected points to the actual item in items array.
Because items IS proxied, changing selected.name
actually changes items[1].name!
-->
{#if selected}
<input bind:value={selected.name} />
{/if} Overriding Derived Values (Optimistic UI)
Derived Values Can Be Temporarily OverriddenAssigning directly to a derived variable overrides it until the next time its source dependencies change, at which point Svelte re-derives it. This is intentional and useful for optimistic UI — but it means the override is temporary, not permanent.
Derived values can be temporarily overridden—perfect for optimistic updates:
<script>
let { post } = $props()
// Normally derived from the server data
let likes = $derived(post.likes)
async function handleLike() {
// Immediately show the like (optimistic)
likes++
try {
// Tell the server
await fetch(`/api/posts/${post.id}/like`, { method: 'POST' })
// Server will eventually update post.likes
} catch (error) {
// Failed! Roll back the optimistic update
likes--
}
}
</script>
<button onclick={handleLike}>
{likes}
</button> The override is temporary—once post.likes changes from the server, likes will sync back to the derived value.
Destructuring with $derived
Svelte 5 handles destructuring in $derived declarations specially—each destructured variable becomes its own reactive derived value:
<script>
let data = $state({ width: 10, height: 20 })
// This WORKS in Svelte 5!
// Each variable becomes its own derived value
let { area, perimeter } = $derived({
area: data.width * data.height,
perimeter: 2 * (data.width + data.height)
})
</script>
<p>Area: {area}</p><p>Perimeter: {perimeter}</p> This is roughly equivalent to:
<script>
let _computed = $derived({
area: data.width * data.height,
perimeter: 2 * (data.width + data.height)
})
let area = $derived(_computed.area)
let perimeter = $derived(_computed.perimeter)
</script> However, if you destructure the result of a $derived value after it’s created (not in the declaration), you lose reactivity:
<script>
let data = $state({ width: 10, height: 20 })
// AVOID: This takes a snapshot, not a reactive reference
// area and perimeter are static values from the moment of destructuring
let { area, perimeter } = dimensions
// PREFERRED: Access through the derived object
// Use dimensions.area and dimensions.perimeter in your template
let dimensions = $derived({
area: data.width * data.height,
perimeter: 2 * (data.width + data.height)
})
</script> The key distinction:
let { a, b } = $derived(...)→ ✅ Reactive (special compiler handling)let x = $derived(...); let { a, b } = x→ ❌ Not reactive (runtime destructuring)
If you need individual derived values and can’t use the declaration destructuring pattern, define them separately:
<script>
// PREFERRED: Separate derived values
let area = $derived(data.width * data.height)
let perimeter = $derived(2 * (data.width + data.height))
</script> Quick Reference
$derived — Simple Expressions
// Arithmetic
let doubled = $derived(count * 2)
let total = $derived(price + tax + shipping)
// Array methods
let filtered = $derived(items.filter((x) => x > 0))
let sorted = $derived([...items].sort())
let first = $derived(items[0])
let count = $derived(items.length)
// String operations
let upper = $derived(name.toUpperCase())
let message = $derived(`Hello, ${name}!`)
// Ternary conditions
let status = $derived(isActive ? 'Active' : 'Inactive')
let display = $derived(loading ? 'Loading...' : data)
// Chained methods
let result = $derived(
items
.filter((x) => x.active)
.map((x) => x.name)
.join(', ')
)
// Nullish coalescing
let value = $derived(user?.name ?? 'Anonymous') $derived.by — Complex Logic
// Multiple statements and local variables
let stats = $derived.by(() => {
const sum = items.reduce((a, b) => a + b, 0)
const avg = sum / items.length
return { sum, avg }
})
// If/else control flow
let tier = $derived.by(() => {
if (points >= 1000) return 'gold'
if (points >= 500) return 'silver'
return 'bronze'
})
// Switch statements
let icon = $derived.by(() => {
switch (status) {
case 'success':
return '✅'
case 'error':
return '❌'
default:
return '⏳'
}
})
// Loops
let grouped = $derived.by(() => {
const groups = {}
for (const item of items) {
const key = item.category
if (!groups[key]) groups[key] = []
groups[key].push(item)
}
return groups
})
// Try/catch
let parsed = $derived.by(() => {
try {
return { data: JSON.parse(input), error: null }
} catch (e) {
return { data: null, error: e.message }
}
})
// Early returns
let result = $derived.by(() => {
if (!user) return null
if (!user.verified) return { error: 'Not verified' }
return { data: user.profile }
}) Excluding Dependencies
import { untrack } from 'svelte'
let result = $derived.by(() => {
const config = untrack(() => configValue) // Read without tracking
return count * config
}) Destructuring Pattern
// Reactive - special compiler handling
let { area, perimeter } = $derived({
area: width * height,
perimeter: 2 * (width + height)
})
// NOT reactive - runtime destructuring
let dimensions = $derived({ area: width * height })
let { area } = dimensions // loses reactivity Key Takeaways
The $derived rune is essential for building reactive Svelte 5 applications. Here’s what you need to remember:
Use $derived When:
- Your computation is a single expression
- No intermediate variables are needed
- Logic fits on one line (or chains nicely)
- You want to signal “this is straightforward”
Use $derived.by When:
- You need multiple statements
- You need local variables for intermediate values
- You need control flow (if/else, switch, loops)
- You need try/catch error handling
- You need early returns
- The logic is complex enough to benefit from explicit structure
Key Principles:
- Choose based on complexity — Use
$derivedfor simple formulas,$derived.byfor complex logic - Dependencies are tracked at runtime — Svelte only recalculates when values actually used have changed
- Keep computations pure — No side effects. Use
$effectfor side effects - Derived values are memoized — They only recalculate when dependencies change
- Use
untrackto exclude dependencies — When you need to read without creating a dependency
With these patterns mastered, you’ll write cleaner, more efficient reactive code that automatically stays in sync with your data.
See Also
Official Documentation
- Svelte 5: $derived - Official reference
- Runes Tutorial - Interactive tutorial
- Svelte REPL - Try code online
- Migration Guide - Svelte 4 to 5
External Resources
- MDN: Memoization - Understanding memoization
- Svelte Society - Community resources
- Svelte Discord - Get help