teta.so
Svelte 5RunesReactivityTutorial

Svelte 5 Runes: The Complete Guide to $state, $derived, and $effect

AI Generated

Introduction

Svelte 5 introduces a fundamentally new way to handle reactivity: runes. Runes are special compiler instructions prefixed with $ that replace Svelte 4's reactive declarations ($:), stores, and other reactivity patterns.

If you are coming from Svelte 4, runes might feel like a big change. But once you understand them, you will find they are more explicit, more composable, and easier to reason about. This guide covers every rune you need to know.

What Are Runes?

Runes are function-like symbols that the Svelte compiler recognizes and transforms. They look like regular JavaScript function calls, but they are actually compiler directives that generate reactive code under the hood.

The key runes are:

Rune Purpose
$state Declare reactive state
$state.raw Declare shallowly reactive state
$derived Computed values from state
$derived.by Computed values with a function
$effect Side effects that react to changes
$effect.pre Effects that run before DOM updates
$props Component props
$bindable Two-way bindable props
$inspect Debug reactive values

$state: Reactive State

The $state rune declares reactive state. When state changes, everything that depends on it updates automatically.

Basic Usage

<script lang="ts">
  let count = $state(0);
  let name = $state('world');
</script>

<button onclick={() => count++}>
  Clicked {count} times
</button>

<input bind:value={name} />
<p>Hello, {name}!</p>

Every time count is incremented or name changes, the DOM updates automatically. No need for setState, no need for signals libraries, no need for stores.

Objects and Arrays

$state makes objects and arrays deeply reactive:

<script lang="ts">
  let todos = $state([
    { id: 1, text: 'Learn Svelte 5', done: false },
    { id: 2, text: 'Build an app', done: false }
  ]);

  function addTodo() {
    todos.push({
      id: Date.now(),
      text: 'New todo',
      done: false
    });
  }

  function toggleTodo(id: number) {
    const todo = todos.find(t => t.id === id);
    if (todo) todo.done = !todo.done;
  }
</script>

{#each todos as todo}
  <label>
    <input
      type="checkbox"
      checked={todo.done}
      onchange={() => toggleTodo(todo.id)}
    />
    <span class:done={todo.done}>{todo.text}</span>
  </label>
{/each}

<button onclick={addTodo}>Add Todo</button>

Notice how you can directly mutate the array with push() and modify object properties. Svelte 5 tracks these mutations automatically thanks to deep proxying.

$state.raw: Shallow Reactivity

If you have large objects or arrays where you do not need deep reactivity (you always replace the whole value), use $state.raw for better performance:

<script lang="ts">
  let items = $state.raw([1, 2, 3]);

  function addItem() {
    // Must replace the entire array (mutation won't trigger updates)
    items = [...items, items.length + 1];
  }
</script>

<p>{items.join(', ')}</p>
<button onclick={addItem}>Add Item</button>

With $state.raw, Svelte does not proxy the value. Only reassignment triggers reactivity, not mutation. This is useful for large datasets, immutable data patterns, or values from external libraries.

Class Fields

$state works beautifully in class definitions:

class Counter {
  count = $state(0);
  name = $state('Counter');

  increment() {
    this.count++;
  }

  reset() {
    this.count = 0;
  }
}

You can use instances of this class in your components, and the reactivity just works.

$derived: Computed Values

The $derived rune creates values that automatically update when their dependencies change. It replaces Svelte 4's $: reactive declarations.

Basic Usage

<script lang="ts">
  let count = $state(0);
  let doubled = $derived(count * 2);
  let isEven = $derived(count % 2 === 0);
</script>

<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<p>{isEven ? 'Even' : 'Odd'}</p>

<button onclick={() => count++}>Increment</button>

$derived takes an expression and evaluates it whenever any reactive value it reads changes.

$derived.by: Complex Computations

For computations that need more than a single expression, use $derived.by with a function:

<script lang="ts">
  let items = $state([
    { name: 'Apple', price: 1.5, quantity: 3 },
    { name: 'Banana', price: 0.75, quantity: 6 },
    { name: 'Cherry', price: 3.0, quantity: 2 }
  ]);

  let cartSummary = $derived.by(() => {
    let total = 0;
    let itemCount = 0;

    for (const item of items) {
      total += item.price * item.quantity;
      itemCount += item.quantity;
    }

    return {
      total: total.toFixed(2),
      itemCount,
      averagePrice: (total / itemCount).toFixed(2)
    };
  });
</script>

<p>Items: {cartSummary.itemCount}</p>
<p>Total: ${cartSummary.total}</p>
<p>Average price per item: ${cartSummary.averagePrice}</p>

The function passed to $derived.by can contain loops, conditionals, and any JavaScript logic. It re-runs automatically whenever any reactive value it reads changes.

Derived Values Are Read-Only

Attempting to assign to a derived value will cause a compiler error:

<script lang="ts">
  let count = $state(0);
  let doubled = $derived(count * 2);

  // This will NOT work:
  // doubled = 10; // Error!
</script>

This is intentional. Derived values are always computed from their source state.

$effect: Side Effects

The $effect rune runs code whenever its reactive dependencies change. It replaces $: statements that performed side effects in Svelte 4.

Basic Usage

<script lang="ts">
  let count = $state(0);

  $effect(() => {
    console.log('Count changed to:', count);
  });
</script>

<button onclick={() => count++}>Count: {count}</button>

The effect runs once initially, then re-runs whenever count changes. Svelte automatically tracks which reactive values are read inside the effect.

Cleanup Functions

Effects can return a cleanup function that runs before the effect re-runs or when the component is destroyed:

<script lang="ts">
  let interval = $state(1000);

  $effect(() => {
    const id = setInterval(() => {
      console.log('tick');
    }, interval);

    return () => {
      clearInterval(id);
    };
  });
</script>

<input type="range" min="100" max="5000" bind:value={interval} />
<p>Interval: {interval}ms</p>

When interval changes, the old interval is cleared before a new one is set up.

$effect.pre: Before DOM Updates

$effect.pre runs before the DOM is updated, which is useful for things like measuring elements before a change:

<script lang="ts">
  let messages = $state<string[]>([]);
  let container: HTMLDivElement;

  $effect.pre(() => {
    // This runs before the DOM updates with new messages
    if (container) {
      const isScrolledToBottom =
        container.scrollHeight - container.scrollTop === container.clientHeight;

      if (isScrolledToBottom) {
        // Schedule scroll after DOM update
        tick().then(() => {
          container.scrollTop = container.scrollHeight;
        });
      }
    }
    // Access messages to track the dependency
    messages.length;
  });
</script>

When NOT to Use $effect

Effects should be used sparingly. Common misuses include:

<script lang="ts">
  let firstName = $state('John');
  let lastName = $state('Doe');

  // BAD: Use $derived instead
  let fullName = $state('');
  $effect(() => {
    fullName = `${firstName} ${lastName}`;
  });

  // GOOD: Use $derived
  let fullNameGood = $derived(`${firstName} ${lastName}`);
</script>

If you are synchronizing state, you almost certainly want $derived instead.

$props: Component Props

The $props rune replaces export let for declaring component props:

<!-- Button.svelte -->
<script lang="ts">
  interface Props {
    label: string;
    variant?: 'primary' | 'secondary';
    disabled?: boolean;
    onclick?: () => void;
  }

  let {
    label,
    variant = 'primary',
    disabled = false,
    onclick
  }: Props = $props();
</script>

<button
  class={variant}
  {disabled}
  {onclick}
>
  {label}
</button>

<style>
  .primary {
    background: #0070f3;
    color: white;
  }
  .secondary {
    background: #eee;
    color: #333;
  }
</style>

Rest Props

Spread remaining props with the rest pattern:

<script lang="ts">
  let { href, children, ...rest }: {
    href: string;
    children: import('svelte').Snippet;
    [key: string]: any;
  } = $props();
</script>

<a {href} {...rest}>
  {@render children()}
</a>

$bindable: Two-Way Binding

The $bindable rune marks a prop as bindable, allowing parent components to use bind: on it:

<!-- TextInput.svelte -->
<script lang="ts">
  let { value = $bindable('') }: { value: string } = $props();
</script>

<input bind:value />

Usage in a parent component:

<script lang="ts">
  import TextInput from './TextInput.svelte';

  let name = $state('');
</script>

<TextInput bind:value={name} />
<p>You typed: {name}</p>

Without $bindable, attempting to use bind:value on the component would cause a compiler error. This makes the binding contract explicit.

$inspect: Debugging

The $inspect rune logs reactive values when they change, similar to console.log but reactive:

<script lang="ts">
  let count = $state(0);
  let name = $state('world');

  $inspect(count);             // Logs count on every change
  $inspect(count, name);       // Logs both when either changes

  // Custom handler
  $inspect(count).with((type, value) => {
    if (type === 'update') {
      debugger; // Pause in dev tools when count changes
    }
  });
</script>

$inspect is automatically stripped from production builds, so you can leave it in your code during development without any performance impact.

Migrating from Svelte 4

Here is a quick reference for translating Svelte 4 patterns to Svelte 5 runes:

Reactive Variables

<!-- Svelte 4 -->
<script>
  let count = 0;
</script>

<!-- Svelte 5 -->
<script>
  let count = $state(0);
</script>

Reactive Declarations

<!-- Svelte 4 -->
<script>
  let count = 0;
  $: doubled = count * 2;
</script>

<!-- Svelte 5 -->
<script>
  let count = $state(0);
  let doubled = $derived(count * 2);
</script>

Reactive Statements

<!-- Svelte 4 -->
<script>
  let count = 0;
  $: console.log(count);
</script>

<!-- Svelte 5 -->
<script>
  let count = $state(0);
  $effect(() => {
    console.log(count);
  });
</script>

Component Props

<!-- Svelte 4 -->
<script>
  export let name = 'world';
</script>

<!-- Svelte 5 -->
<script>
  let { name = 'world' } = $props();
</script>

Stores to Runes

<!-- Svelte 4 (with stores) -->
<script>
  import { writable } from 'svelte/store';
  const count = writable(0);
</script>
<button on:click={() => $count++}>{$count}</button>

<!-- Svelte 5 (with runes) -->
<script>
  let count = $state(0);
</script>
<button onclick={() => count++}>{count}</button>

Summary

Svelte 5 runes provide a cleaner, more explicit reactivity system:

  • $state for reactive variables (replaces implicit reactivity and stores)
  • $state.raw for shallow reactivity when you replace, not mutate
  • $derived for computed values (replaces $: declarations)
  • $derived.by for complex computations with a function body
  • $effect for side effects (replaces $: statements with side effects)
  • $effect.pre for effects that need to run before DOM updates
  • $props for component props (replaces export let)
  • $bindable for two-way bindable props
  • $inspect for debugging reactive values

The migration from Svelte 4 is straightforward, and the new system makes reactivity more predictable and easier to understand. Your components become more explicit about what is reactive and how data flows.

Next Steps

Source: Teta Engineering

This content was generated with AI from public sources. It represents analysis and commentary, not original journalism.

Build your next app with AI

Try Teta — create sites and apps with AI agents.

Get started free