Svelte 5 Runes: A First Look at the New Reactivity System

Svelte 5 introduces runes - a new reactivity system that changes how we write reactive components. Here's what you need to know.

Svelte 5 Runes: A First Look at the New Reactivity System

Table of Contents

What’s New in Svelte 5

Svelte 5 represents the biggest shift in the framework’s history. Gone is the familiar export let syntax and the $: reactive statements. In their place come runes - a new primitive system for reactivity that mirrors what frameworks like React have been moving toward with hooks.

The key difference? Runes are explicit. You tell Svelte exactly what is reactive, rather than relying on the compiler to infer it from assignment patterns.

The Runes Overview

Svelte 5 introduces four primary runes:

  • $state() - Declares reactive state
  • $derived() - Creates computed values
  • $effect() - Runs side effects when dependencies change
  • $props() - Defines component props (replacing export let)

Each rune signals to the compiler: “pay attention to this.”

$state - Reactive Variables

In Svelte 4, any variable could become reactive simply by changing it in the template. In Svelte 5, you opt-in:

<script>
  let count = $state(0);
  
  function increment() {
    count += 1;
  }
</script>

<button onclick={increment}>
  Count: {count}
</button>

The key difference: count is now deeply reactive. You can pass it to child components and changes will propagate automatically.

For objects, use $state with an initial object:

let user = $state({ name: 'Alice', age: 30 });

function birthday() {
  user.age += 1;  // This triggers updates!
}

$derived - Computed Values

Where you’d previously use $: doubled = count * 2, now you use $derived:

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

<p>Double is {doubled}</p>

The value updates automatically when count changes. No more dependency arrays to manage, no more subtle bugs from forgotten dependencies.

$effect - Side Effects

Replacing onMount and reactive statements is $effect:

<script>
  let count = $state(0);
  
  $effect(() => {
    // This runs when count changes
    console.log('Count is now:', count);
  });
</script>

For cleanup, return a function:

$effect(() => {
  const controller = new AbortController();
  
  fetch('/api/data', { signal: controller.signal })
    .then(/* ... */);
    
  return () => controller.abort();
});

This replaces the need for reactive statements ($:) for side effects and consolidates lifecycle handling.

Migrating from Svelte 4

Here’s a quick reference for common migrations:

Svelte 4Svelte 5
export let foo;let { foo } = $props();
let count = 0;let count = $state(0);
$: doubled = count * 2;let doubled = $derived(count * 2);
$: { console.log(count); }$effect(() => console.log(count));
onMount(() => {})$effect(() => {}) (in component)

The migration isn’t all-or-nothing. Svelte 5 is backwards compatible, so you can migrate incrementally.

Why Runes Matter

The explicit reactivity model has several advantages:

1. Clarity - When you see $state, you know exactly what’s reactive. No more guessing which variables will trigger updates.

2. Debugging - Reactive statements in Svelte 4 could be tricky to debug. Runes make the flow explicit.

3. Portability - Runes work the same inside and outside .svelte files. You can use Svelte’s reactivity in plain JavaScript modules.

4. Performance - The compiler can optimize more aggressively when it knows exactly what to track.


In This Project

This blog itself uses Svelte 5. The Bluesky thread component (BlueskyThread.svelte) is a good example of where runes shine. Here’s how it handles state and props:

<script>
  // Props using $props() - the Svelte 5 way
  interface Props {
    uri: string;
    initialCount?: number;
    incrementBy?: number;
    maxDepth?: number;
    path: URL;
  }

  let { 
    uri, 
    initialCount = 5, 
    incrementBy = 5, 
    maxDepth = 3, 
    path 
  }: Props = $props();

  // Component state using $state
  let allReplies = $state<any[]>([]);
  let visibleCount = $state(0);
  let meta = $state<any>(null);
  let loading = $state(true);
  let error = $state<string | null>(null);

  // Initialize from props with $effect
  $effect(() => {
    visibleCount = initialCount;
  });

  // Derived values with $derived
  let visibleReplies = $derived(allReplies.slice(0, visibleCount));
</script>

Instead of relying on implicit reactivity or managing reactive statements, the runes make it clear what’s happening. allReplies is the source of truth, visibleReplies is derived from it, and changes flow naturally. Props are now explicitly declared with $props() instead of export let.

If you’re starting a new Svelte project, I’d recommend going straight to Svelte 5. The syntax is cleaner, the mental model is simpler, and the framework is moving in a direction that makes sense for modern web development.

Browse Articles by Topic

Latest Articles

...
Go to top of page