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.

Try Svelte 5 in the REPL


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 Action

Svelte’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:

RunePurposeExample UseLearn More
$stateCreates reactive state (data that triggers UI updates)User input, counters, togglesTutorial
$state.rawCreates shallow reactive state (for performance)Large arrays from APIsTutorial
$state.snapshotGets a plain, non-reactive copy of stateLogging, API requestsTutorial
$derivedCreates computed values from a single expressionTotals, formatted stringsTutorial
$derived.byCreates computed values with complex logic (multiple statements)Complex calculations, filteringTutorial
$effectRuns side effects when dependencies changeAnalytics, localStorageTutorial
$effect.preRuns before DOM updatesSaving scroll positionTutorial
$propsDeclares component propsComponent inputsTutorial
$bindableMakes a prop two-way bindableForm inputs, custom controlsTutorial
$inspectDebug helper for logging reactive value changesDevelopment debuggingTutorial
$hostAccess custom element hostWeb componentsTutorial

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 tip

Use $state.raw for 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 tip

Derived 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>
Important

Most of the time you don’t need $effect! If you’re computing a value from other values, use $derived instead. Only use $effect for 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:

LocationWorks?Notes
.svelte component filesBoth <script> and template
.svelte.js / .svelte.ts filesFor shared reactive state
Regular .js / .ts filesCompiler 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 4Svelte 5
let count = 0let count = $state(0)
$: double = count * 2let double = $derived(count * 2)
$: { console.log(count) }$effect(() => { console.log(count) })
$: if (count > 10) {...}$effect(() => { if (count > 10) {...} })

Component API

Svelte 4Svelte 5
export let namelet { name } = $props()
export let valuelet { value = $bindable() } = $props()
on:click={handler}onclick={handler}
<slot />{@render children()}

Lifecycle

Svelte 4Svelte 5
onMount(() => {...})$effect(() => {...})
beforeUpdate(() => {...})$effect.pre(() => {...})
afterUpdate(() => {...})$effect(() => {...})
onDestroy(() => {...})$effect(() => { return () => {...} })

Complete Migration Guide


Learning Path Recommendations

For Complete Beginners

  1. $state Tutorial - Start here! Master reactive state
  2. $derived Tutorial - Learn computed values
  3. $props Tutorial - Component communication
  4. Build a small project to practice!

For React/Vue Developers

  1. Fine-Grained Reactivity - You’re here!
  2. $state Tutorial - Compare with useState/ref
  3. $derived Tutorial - Compare with useMemo/computed
  4. $effect Tutorial - Compare with useEffect/watchEffect

For Svelte 4 Developers

  1. Migration Guide - Official guide
  2. $state Tutorial - Replace reactive declarations
  3. $effect Tutorial - Replace lifecycle hooks
  4. $props Tutorial - New props system

For Advanced Patterns

  1. Advanced Props Patterns - Rest props, generics
  2. $host Tutorial - Web components
  3. $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:

  • $state for reactive data
  • $derived for computed values
  • $effect for side effects
  • $props for component communication
  • $bindable for 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

External Resources