Introduction
When you're building Svelte components and SvelteKit applications, you'll quickly encounter two powerful tools for managing state and side effects: $derived and $effect. Understanding when to reach for each one is the difference between clean, performant code and the kind of debugging session that eats half your afternoon. This $derived vs $effect: a practical Svelte & SvelteKit guide walks through real scenarios we've encountered in production, so you can make the right choice every time.
$derived for values from state
$derived is a reactive declaration that computes a new value based on one or more pieces of state. Think of it as a computed property that automatically updates whenever its dependencies change. The key insight: $derived returns a value. It doesn't do anything; it just calculates something new.
Let's say we're building a cart component for an e-commerce platform. We have items and want to compute the total price and tax:
<script>
let items = [
{ id: 1, price: 29.99, quantity: 2 },
{ id: 2, price: 49.99, quantity: 1 }
];
let taxRate = 0.08;
let subtotal = $derived(
items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
);
let tax = $derived(subtotal * taxRate);
let total = $derived(subtotal + tax);
</script>
<p>Subtotal: ${subtotal.toFixed(2)}</p>
<p>Tax: ${tax.toFixed(2)}</p>
<p>Total: ${total.toFixed(2)}</p>
Every time items or taxRate changes, subtotal recalculates. And because tax depends on subtotal, it automatically updates too. No manual synchronization, no stale values. This is the pattern you want for data transformation: filtering a list, mapping values, combining properties.
The constraint is that $derived must be synchronous and pure. You can't call API endpoints or log to external systems inside a $derived block. If you need multi-line logic, there's a variant for that.
$effect for DOM and external side effects
$effect is fundamentally different. It's designed for side effects: things that happen as a consequence of state changes. Think DOM manipulation, API calls, analytics events, or subscribing to external systems. When state changes, the effect runs. That's its job.
Here's a common scenario: a CRM form where we fetch contact details when the user ID changes, and we also want to focus a specific input field after the data loads.
<script>
import { onMount } from 'svelte';
let userId = null;
let contactData = null;
let loading = false;
let emailInput;
$effect(() => {
if (!userId) return;
loading = true;
fetch(`/api/contacts/${userId}`)
.then(res => res.json())
.then(data => {
contactData = data;
loading = false;
});
});
$effect(() => {
if (contactData && emailInput) {
emailInput.focus();
}
});
</script>
<input bind:this={emailInput} type="email" value={contactData?.email} />
When userId changes, the first effect triggers: we fetch data from the API. When contactData updates, the second effect runs: we focus the email input. These are synchronous reactions to state changes, and that's exactly what $effect is for.
One important detail: $effect runs after the component mounts and after render. If you need something to run during render or setup, use onMount or initialization code instead. If you're in a SvelteKit load function, you probably don't need effects at all.
Avoid setting state inside effects
This is where teams run into trouble. It's tempting to update state inside an effect. Don't do it, or at least do it very carefully.
The problem: if an effect updates state that the effect depends on, you can create an infinite loop or unpredictable cascades. Consider this anti-pattern:
<script>
let count = 0;
let doubled = 0;
// Bad: doubled is changed inside an effect
$effect(() => {
doubled = count * 2;
});
</script>
<button on:click={() => count++}>Increment</button>
<p>Count: {count}, Doubled: {doubled}</p>
This works, but it's backwards. doubled is derived from count, so it should be a $derived. Using an effect here makes the dependency invisible and harder to reason about.
The rule: if you're calculating a new value from state, use $derived. If you're synchronizing external state (like a form validity status from an API validation), or triggering behavior, use an effect. State mutations in effects are okay when you're truly reacting to external events or coordinating multiple systems. But nine times out of ten, if you're writing derived = source inside an effect, you should use $derived instead.
$derived.by for multi-line logic
$derived works for single expressions, but sometimes you need multiple statements before returning a value. That's where $derived.by() comes in. It accepts a function and returns whatever that function returns.
Imagine a ride-hailing app where we're calculating fare based on distance, time of day, and surge pricing:
<script>
let distance = 5.2; // miles
let duration = 18; // minutes
let surgeMultiplier = 1.5;
let fare = $derived.by(() => {
const baseRate = 2.5;
const perMile = 1.25;
const perMinute = 0.25;
const distanceCost = distance * perMile;
const timeCost = duration * perMinute;
const subtotal = baseRate + distanceCost + timeCost;
const final = subtotal * surgeMultiplier;
return Math.round(final * 100) / 100; // Round to 2 decimals
});
</script>
<p>Fare: ${fare}</p>
The function passed to $derived.by is called every time a dependency changes. It's synchronous and pure, just like inline $derived. The main difference is readability: when you have complex logic, wrapping it in a function makes intent clearer and debugging easier.
You can also use $derived.by to conditionally compute values or handle errors gracefully:
let validationResult = $derived.by(() => {
if (!email) return { valid: false, message: 'Email required' };
if (!email.includes('@')) return { valid: false, message: 'Invalid email format' };
return { valid: true, message: 'OK' };
});
Common mistakes
Mistake 1: Using effects when $derived would work. We see this often when engineers coming from Vue or React default to the effects pattern. Remember: if you're reading state and returning a value, it's $derived. If you're triggering behavior or synchronizing external systems, it's $effect.
Mistake 2: Mutating state inside derived. $derived must be pure. You can't do this:
let count = 0;
let sideEffect = $derived(() => {
count++; // Don't do this!
return count;
});
Mistake 3: Forgetting that effects run after render. If you set state inside an effect, your component will render with old values, then re-render with new ones. This causes layout shift and performance issues. If you need values available on first render, initialize them or use a load function in SvelteKit.
Mistake 4: Creating infinite loops with effect dependencies. An effect that modifies its own dependency will re-run immediately. Always double-check what you're reading and writing in effects.
Key takeaways
- Use
$derivedto compute values from state. It's reactive, pure, and synchronous. - Use
$effectto handle side effects: DOM updates, API calls, external subscriptions, and behavior coordination. - Avoid mutating state inside effects unless you're coordinating multiple systems. Most computed values should be
$derived. - Use
$derived.bywhen you need multi-line logic. It keeps complex calculations readable and testable. - Effects run after render. If you need to set up initial state, do it during component initialization, not in effects.
- Think about dependencies carefully. Both
$derivedand$effectare reactive, but$deriveddeclares dependencies implicitly while$effectdepends on closure variables.






Comments
Loading comments…