Introduction to Svelte 5 Reactivity
Svelte 5 introduced a revolutionary approach to reactivity with runes. This guide provides an overview of how reactivity works in Svelte 5 and introduces you to the runes that make it all possible. For in-depth tutorials on each rune, follow the links throughout this article.
What You Should Know
Before diving in, make sure you’re comfortable with these JavaScript concepts:
- Variables:
let,const, and how they differ - Objects and Arrays: Creating, accessing, and modifying them
- Functions: How to define and call functions
- Arrow Functions: The
() => {}syntax - Basic DOM concepts: What happens when a webpage updates
If you’re not sure about any of these, that’s okay! The individual rune tutorials explain things as they go, but having this foundation will help.
Why Reactivity Matters
The Problem: Keeping UI in Sync with Data
Imagine you’re building a simple counter. In plain JavaScript, you’d write something like this:
<button id="btn">Clicks: 0</button>
<script>
let count = 0
const button = document.getElementById('btn')
button.addEventListener('click', () => {
count++
button.textContent = `Clicks: ${count}` // Manual update!
})
</script> Notice the problem? Every time count changes, you manually have to update the button text. In a simple example, that’s fine. But imagine a complex app with dozens of variables affecting hundreds of UI elements. You’d spend more time writing update code than actual features!
The Solution: Reactive Programming
Reactivity means the UI automatically updates when your data changes. You just change the data, and the framework handles the rest.
Think of it like a spreadsheet: When you change cell A1, any formula that references A1 automatically recalculates. You don’t manually update each cell—Excel does it for you.
Svelte works the same way. You change a variable, and any part of the UI using that variable automatically updates:
<script>
let count = $state(0)
</script>
<button onclick={() => count++}>
Clicks: {count}
</button> No manual DOM updates. No event listeners to manage. Just change the data and Svelte handles the rest.
Reactivity in ActionSvelte’s reactivity is “fine-grained”, meaning only the parts of the DOM that need updating are changed. Clicking the button above only updates the text node—the button element itself isn’t re-rendered.
What Are Runes?
In Svelte 5, runes are special symbols (starting with $) that tell Svelte how to handle reactivity. The word “rune” comes from ancient symbols with magical properties—fitting, since they add “magic” to your variables!
Visual: How Runes Transform Your Code
You write: Svelte compiles to:
───────────────────────────────────────────────────
let count = $state(0) → let count = signal(0)
let doubled = $derived( → let doubled = computed(
count * 2 () => count * 2
) )
$effect(() => { → effect(() => {
console.log(count) console.log(count)
}) }) Runes are compiler directives—they tell the Svelte compiler how to transform your code into efficient reactive JavaScript.
Complete Runes Reference
Here are all the main runes and what they do:
| Rune | Purpose | Example Use | Learn More |
|---|---|---|---|
$state | Creates reactive state (data that triggers UI updates) | User input, counters, toggles | Tutorial |
$state.raw | Creates shallow reactive state (for performance) | Large arrays from APIs | Tutorial |
$state.snapshot | Gets a plain, non-reactive copy of state | Logging, API requests | Tutorial |
$derived | Creates computed values from a single expression | Totals, formatted strings | Tutorial |
$derived.by | Creates computed values with complex logic (multiple statements) | Complex calculations, filtering | Tutorial |
$effect | Runs side effects when dependencies change | Analytics, localStorage | Tutorial |
$effect.pre | Runs before DOM updates | Saving scroll position | Tutorial |
$props | Declares component props | Component inputs | Tutorial |
$bindable | Makes a prop two-way bindable | Form inputs, custom controls | Tutorial |
$inspect | Debug helper for logging reactive value changes | Development debugging | Tutorial |
$host | Access custom element host | Web components | Tutorial |
Quick Overview of Each Rune
$state — Reactive State
The foundation of Svelte 5 reactivity. Use $state to create variables that trigger UI updates when changed:
<script>
let count = $state(0)
let user = $state({ name: 'Alice', age: 30 })
let items = $state(['apple', 'banana'])
</script>
<button onclick={() => count++}>Count: {count}</button><p>Name: {user.name}</p> Objects and arrays are deeply reactive—changes to nested properties automatically trigger updates. For large datasets you replace entirely, use $state.raw for better performance.
Performance tipUse
$state.rawfor arrays with 1000+ items that you replace entirely (like API responses). It’s 3-5x faster than deep proxying.
$derived — Computed Values
Creates values that automatically recalculate when their dependencies change:
<script>
let price = $state(100)
let quantity = $state(2)
let total = $derived(price * quantity)
</script>
<p>Total: ${total}</p> For complex computations with multiple statements, use $derived.by:
<script>
let items = $state([1, 2, 3, 4, 5])
let stats = $derived.by(() => {
const sum = items.reduce((a, b) => a + b, 0)
const avg = sum / items.length
return { sum, avg }
})
</script>
<p>Sum: {stats.sum}, Average: {stats.avg}</p> Performance tipDerived values are automatically memoized—they only recalculate when dependencies change, making them perfect for expensive computations.
$effect — Side Effects
Runs code when reactive dependencies change. Perfect for syncing with external systems, logging, or any operation that shouldn’t be part of rendering:
<script>
let count = $state(0)
$effect(() => {
console.log('Count changed to:', count)
document.title = `Count: ${count}`
})
</script>
<button onclick={() => count++}>Increment</button> ImportantMost of the time you don’t need
$effect! If you’re computing a value from other values, use$derivedinstead. Only use$effectfor side effects like API calls, localStorage, or integrating with non-Svelte code.
$props — Component Props
Declares what data a component receives from its parent:
<script>
let { name, age = 0 } = $props()
</script>
<p>{name} is {age} years old</p> $bindable — Two-Way Binding
Makes a prop capable of two-way binding, allowing child components to update parent state:
<!-- TextInput.svelte -->
<script>
let { value = $bindable() } = $props()
</script>
<input bind:value /> <!-- Parent.svelte -->
<script>
import TextInput from './TextInput.svelte'
let name = $state('')
</script>
<TextInput bind:value={name} /><p>You typed: {name}</p> Where Can You Use Runes?
Runes work in:
| Location | Works? | Notes |
|---|---|---|
.svelte component files | ✅ | Both <script> and template |
.svelte.js / .svelte.ts files | ✅ | For shared reactive state |
Regular .js / .ts files | ❌ | Compiler doesn’t process these |
This is because runes are transformed by the Svelte compiler. Regular JavaScript files aren’t processed by the compiler, so runes won’t work there.
Svelte 4 vs Svelte 5: Migration Guide
If you’re coming from Svelte 4, here’s how the syntax has changed:
State Management
| Svelte 4 | Svelte 5 |
|---|---|
let count = 0 | let count = $state(0) |
$: double = count * 2 | let double = $derived(count * 2) |
$: { console.log(count) } | $effect(() => { console.log(count) }) |
$: if (count > 10) {...} | $effect(() => { if (count > 10) {...} }) |
Component API
| Svelte 4 | Svelte 5 |
|---|---|
export let name | let { name } = $props() |
export let value | let { value = $bindable() } = $props() |
on:click={handler} | onclick={handler} |
<slot /> | {@render children()} |
Lifecycle
| Svelte 4 | Svelte 5 |
|---|---|
onMount(() => {...}) | $effect(() => {...}) |
beforeUpdate(() => {...}) | $effect.pre(() => {...}) |
afterUpdate(() => {...}) | $effect(() => {...}) |
onDestroy(() => {...}) | $effect(() => { return () => {...} }) |
Learning Path Recommendations
For Complete Beginners
- $state Tutorial - Start here! Master reactive state
- $derived Tutorial - Learn computed values
- $props Tutorial - Component communication
- Build a small project to practice!
For React/Vue Developers
- Fine-Grained Reactivity - You’re here!
- $state Tutorial - Compare with useState/ref
- $derived Tutorial - Compare with useMemo/computed
- $effect Tutorial - Compare with useEffect/watchEffect
For Svelte 4 Developers
- Migration Guide - Official guide
- $state Tutorial - Replace reactive declarations
- $effect Tutorial - Replace lifecycle hooks
- $props Tutorial - New props system
For Advanced Patterns
- Advanced Props Patterns - Rest props, generics
- $host Tutorial - Web components
- $inspect Tutorial - Advanced debugging
Quick Reference & Common Patterns
1. Counter with Multiple Operations
<script>
let count = $state(0)
let step = $state(1)
let total = $derived(count * step)
</script>
<button onclick={() => (count += step)}>Add {step}</button>
<button onclick={() => (count -= step)}>Subtract {step}</button>
<button onclick={() => (count = 0)}>Reset</button>
<input type="number" bind:value={step} />
<p>Count: {count}</p>
<p>Total: {total}</p> 2. Form with Validation
<script>
let email = $state('')
let password = $state('')
let isValidEmail = $derived(email.includes('@'))
let isValidPassword = $derived(password.length >= 8)
let canSubmit = $derived(isValidEmail && isValidPassword)
</script>
<input bind:value={email} type="email" />
{#if !isValidEmail && email}
<span class="error">Invalid email</span>
{/if}
<input bind:value={password} type="password" />
{#if !isValidPassword && password}
<span class="error">Password too short</span>
{/if}
<button disabled={!canSubmit}>Submit</button> 3. Data Fetching
<script>
let userId = $state(1)
let user = $state(null)
let loading = $state(false)
$effect(() => {
// Capture userId synchronously so it becomes a tracked dependency
const id = userId
const controller = new AbortController()
loading = true
fetch(`/api/users/${id}`, { signal: controller.signal })
.then((r) => r.json())
.then((data) => {
user = data
loading = false
})
.catch((err) => {
if (err.name !== 'AbortError') loading = false
})
// Cancel the previous request when userId changes before it resolves
return () => controller.abort()
})
</script>
<input type="number" bind:value={userId} />
{#if loading}
<p>Loading...</p>
{:else if user}
<p>{user.name}</p>
{/if} 4. Local Storage Sync
<script>
let theme = $state(localStorage.getItem('theme') ?? 'light')
$effect(() => {
localStorage.setItem('theme', theme)
document.body.className = theme
})
</script>
<button onclick={() => (theme = theme === 'light' ? 'dark' : 'light')}> Toggle Theme </button> Decision Tree: Which Rune to Use?
Need to store data that changes?
↓
YES → Use $state
Need to compute a value from other values?
↓
YES → Use $derived (or $derived.by for complex logic)
Need to sync with external system (API, localStorage, etc.)?
↓
YES → Use $effect
Receiving data from parent component?
↓
YES → Use $props
Need child to update parent's data?
↓
YES → Use $bindable (or callback props)
Need to debug reactive values?
↓
YES → Use $inspect Key Takeaways
Svelte 5’s runes system provides a powerful, intuitive way to build reactive applications:
$statefor reactive data$derivedfor computed values$effectfor side effects$propsfor component communication$bindablefor two-way binding
The syntax is clean, close to vanilla JavaScript, and the compiler handles all the complexity behind the scenes. Start with $state and $derived, and you’ll have everything you need for most applications.
See Also
Official Documentation
- Svelte 5 Documentation - Complete reference
- Interactive Tutorial - Learn by doing
- Svelte REPL - Try code online
- Migration Guide - Svelte 4 to 5
Related Articles in This Series
- All Runes Tutorials - Complete series
- SvelteKit with Runes - Full-stack patterns
External Resources
- Svelte Society - Community resources
- Svelte Discord - Get help from the community
- Svelte Summit - Conference talks