The Synchronization Bug
Here’s a bug that gets shipped more often than anyone would like to admit:
A dashboard shows a list of orders and a total. The total is stored in state. When an order is added, you update both the orders list and the total. Except sometimes—under certain conditions, in certain browsers, when the user clicks fast enough—the total doesn’t update.
The orders list shows 5 items. The total shows the sum of 4. The user refreshes, and it’s fine. You can’t reproduce it locally. It happens in production twice a week.
The bug isn’t in the addition logic. The bug is that the total exists as separate state at all.
If a value can be computed from other values, it shouldn’t be stored. It should be derived.
The Synchronization Trap
When you store computed values as state, you take on a responsibility: keep them synchronized.
<script>
let items = $state([
{ name: 'Widget', price: 10 },
{ name: 'Gadget', price: 25 }
]);
// Stored state - YOU are responsible for keeping this correct
let total = $state(35);
function addItem(item) {
items.push(item);
total += item.price; // Don't forget this!
}
function removeItem(index) {
total -= items[index].price; // Or this!
items.splice(index, 1);
}
function updatePrice(index, newPrice) {
total -= items[index].price; // Subtract old
items[index].price = newPrice;
total += newPrice; // Add new... did you remember both?
}
</script> Every function that modifies items must also update total. Miss one, and they’re out of sync. The more places that modify state, the more places bugs can hide.
This is the synchronization trap: storing values that could be computed creates ongoing maintenance burden and inevitable bugs.
Derived State Is Always Correct
Derived state can’t be wrong. It’s computed from source state every time it’s read. There’s no synchronization because there’s nothing to synchronize.
<script>
let items = $state([
{ name: 'Widget', price: 10 },
{ name: 'Gadget', price: 25 }
]);
// Derived - always correct by definition
let total = $derived(items.reduce((sum, item) => sum + item.price, 0));
function addItem(item) {
items.push(item);
// total updates automatically
}
function removeItem(index) {
items.splice(index, 1);
// total updates automatically
}
function updatePrice(index, newPrice) {
items[index].price = newPrice;
// total updates automatically
}
</script> No manual updates. No forgetting. No synchronization bugs. The total is always the sum of the items because that’s what it is.
Simple Derivations with $derived
For expressions that fit on one line, use $derived:
<script>
let firstName = $state('Ada');
let lastName = $state('Lovelace');
let fullName = $derived(`${firstName} ${lastName}`);
let initials = $derived(`${firstName[0]}${lastName[0]}`);
let isLongName = $derived(fullName.length > 15);
</script> The expression inside $derived() is re-evaluated whenever any state it reads changes. Svelte tracks dependencies automatically—you don’t declare them.
Complex Derivations with $derived.by
When you need more than a single expression—loops, conditionals, intermediate variables—use $derived.by:
<script>
let transactions = $state([
{ type: 'income', amount: 1000 },
{ type: 'expense', amount: 200 },
{ type: 'income', amount: 500 },
{ type: 'expense', amount: 150 }
]);
let summary = $derived.by(() => {
let income = 0;
let expenses = 0;
for (const t of transactions) {
if (t.type === 'income') {
income += t.amount;
} else {
expenses += t.amount;
}
}
return {
income,
expenses,
net: income - expenses,
count: transactions.length
};
});
</script>
<p>Income: ${summary.income}</p>
<p>Expenses: ${summary.expenses}</p>
<p>Net: ${summary.net}</p>
<p>Transactions: {summary.count}</p> $derived.by takes a function. The function runs, Svelte tracks what it reads, and the return value becomes the derived state. Same automatic dependency tracking, more room for logic.
Chaining Derived Values
Derived state can depend on other derived state. Svelte builds a dependency graph and updates everything in the right order.
<script>
let items = $state([
{ name: 'Laptop', price: 999, quantity: 1 },
{ name: 'Mouse', price: 49, quantity: 2 },
{ name: 'Keyboard', price: 129, quantity: 1 }
]);
let taxRate = $state(0.08);
let discount = $state(0);
// First level: subtotal depends on items
let subtotal = $derived(
items.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
// Second level: discounted depends on subtotal and discount
let discounted = $derived(subtotal - discount);
// Third level: tax depends on discounted and taxRate
let tax = $derived(discounted * taxRate);
// Fourth level: total depends on discounted and tax
let total = $derived(discounted + tax);
</script>
<p>Subtotal: ${subtotal.toFixed(2)}</p>
<p>Discount: -${discount.toFixed(2)}</p>
<p>Tax ({(taxRate * 100).toFixed(0)}%): ${tax.toFixed(2)}</p>
<p><strong>Total: ${total.toFixed(2)}</strong></p> Change items, and everything recalculates: subtotal, discounted, tax, total. Change taxRate, and only tax and total recalculate. Svelte does the minimum work necessary.
You didn’t have to think about the order. You didn’t have to manually propagate changes. You declared relationships, and Svelte figured out the rest.
View Models Should Be Derived
A powerful pattern: derive your view model from your source state.
Source state is the raw data—what you’d store in a database. View model is what the UI needs—formatted, filtered, sorted, augmented.
<script>
// Source state: raw data
let users = $state([
{ id: 1, name: 'Alice', role: 'admin', lastActive: '2025-01-10' },
{ id: 2, name: 'Bob', role: 'user', lastActive: '2025-01-08' },
{ id: 3, name: 'Carol', role: 'user', lastActive: '2025-01-09' }
]);
let searchQuery = $state('');
let roleFilter = $state('all');
let sortBy = $state('name');
// View model: what the UI needs
let viewModel = $derived.by(() => {
let result = users;
// Filter by search
if (searchQuery) {
const query = searchQuery.toLowerCase();
result = result.filter(u => u.name.toLowerCase().includes(query));
}
// Filter by role
if (roleFilter !== 'all') {
result = result.filter(u => u.role === roleFilter);
}
// Sort
result = [...result].sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
if (sortBy === 'lastActive') return b.lastActive.localeCompare(a.lastActive);
return 0;
});
// Augment with display data
return result.map(user => ({
...user,
displayName: user.name + (user.role === 'admin' ? ' ⭐' : ''),
isRecent: user.lastActive === '2025-01-10'
}));
});
</script>
<input bind:value={searchQuery} placeholder="Search users..." />
<select bind:value={roleFilter}>
<option value="all">All roles</option>
<option value="admin">Admins</option>
<option value="user">Users</option>
</select>
<select bind:value={sortBy}>
<option value="name">Sort by name</option>
<option value="lastActive">Sort by activity</option>
</select>
<ul>
{#each viewModel as user (user.id)}
<li class:recent={user.isRecent}>{user.displayName}</li>
{/each}
</ul> The source data (users) stays clean. The UI concerns (filtering, sorting, display formatting) live in the derived view model. Change any input—search, filter, sort, or the underlying users—and the view model updates.
The Rule
Here’s the rule, and it’s simple:
If a value can be computed from other values, use $derived.
Don’t store it. Don’t synchronize it. Derive it.
This applies more often than you’d think:
- Totals, counts, averages → derived
- Filtered or sorted lists → derived
- Form validation state → derived
- UI flags like
isEmpty,isValid,hasChanges→ derived - Formatted display values → derived
The only state you should store is state that comes from outside: user input, API responses, time. Everything else is derivable.
Performance?
You might worry: “Doesn’t this recalculate on every change? Isn’t that slow?”
Two things:
First, Svelte is smart about when to recalculate. Derived values are only recomputed when their dependencies change and when they’re actually read. Unused derived values don’t cost anything.
Second, even frequent recalculation is usually fast. JavaScript is quick. Reducing an array of 100 items takes microseconds. You’d need thousands of items and complex computations before derived state becomes a performance concern.
When you do hit performance issues—and you probably won’t—Svelte offers $derived with memoization built in. If the inputs haven’t changed, the cached value is returned. This happens automatically for reference-stable dependencies.
Premature optimization is the root of all evil. Derive first, optimize only if you measure a problem.
Next up: We’ve covered $state and $derived. But what about $effect? It’s the most misused rune, and understanding when not to use it is just as important as understanding when you should.