Back to blog

Svelte 5 and Runes: The Reactivity Revolution in JavaScript in 2025

Hello HaWkers, Svelte 5 has arrived with a paradigmatic change that is making the JavaScript community rethink how we approach reactivity in web applications. Runes represent a new way of thinking about state and reactivity, and if you do not know them yet, you are missing one of the most interesting innovations in the frontend ecosystem.

Have you ever wondered why we need so many hooks and boilerplate to manage state in React? Svelte 5 offers an elegant alternative.

What Are Runes?

Runes are a new reactivity primitive introduced in Svelte 5. They replace the previous reactivity system based on reactive declarations ($:) with a more explicit and powerful model.

The Main Runes

List of available Runes:

  • $state - Declares reactive state
  • $derived - Computes derived values
  • $effect - Executes reactive side effects
  • $props - Receives props in components
  • $bindable - Props that can be two-way bound
  • $inspect - Debug reactive values

Comparison: Svelte 4 vs Svelte 5

Let us see in practice how the syntax changed and why it is a significant improvement.

Simple State

Svelte 4 (old):

<script>
  let count = 0;

  function increment() {
    count += 1;
  }
</script>

<button on:click={increment}>
  Clicks: {count}
</button>

Svelte 5 (Runes):

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

  function increment() {
    count += 1;
  }
</script>

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

The difference seems subtle, but $state makes reactivity explicit and predictable.

Derived Values

Svelte 4 (old):

<script>
  let firstName = 'John';
  let lastName = 'Doe';

  // Reactive declaration - can be confusing
  $: fullName = `${firstName} ${lastName}`;
  $: greeting = `Hello, ${fullName}!`;
</script>

Svelte 5 (Runes):

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

  // Much clearer and predictable
  let fullName = $derived(`${firstName} ${lastName}`);
  let greeting = $derived(`Hello, ${fullName}!`);
</script>

Effects (Side Effects)

Svelte 4 (old):

<script>
  let count = 0;

  // Implicit effect - hard to know when it executes
  $: {
    console.log(`Count changed to: ${count}`);
    localStorage.setItem('count', count);
  }
</script>

Svelte 5 (Runes):

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

  // Explicit effect - clear when and why it executes
  $effect(() => {
    console.log(`Count changed to: ${count}`);
    localStorage.setItem('count', count.toString());
  });
</script>

Practical Example: Todo App with Runes

Let us build a complete task application using Runes:

<script>
  // Main state
  let todos = $state([]);
  let newTodo = $state('');
  let filter = $state('all'); // 'all' | 'active' | 'completed'

  // Derived values
  let filteredTodos = $derived(() => {
    switch (filter) {
      case 'active':
        return todos.filter(t => !t.completed);
      case 'completed':
        return todos.filter(t => t.completed);
      default:
        return todos;
    }
  });

  let remainingCount = $derived(
    todos.filter(t => !t.completed).length
  );

  let allCompleted = $derived(
    todos.length > 0 && todos.every(t => t.completed)
  );

  // Action functions
  function addTodo() {
    if (newTodo.trim()) {
      todos.push({
        id: Date.now(),
        text: newTodo.trim(),
        completed: false
      });
      newTodo = '';
    }
  }

  function toggleTodo(id) {
    const todo = todos.find(t => t.id === id);
    if (todo) {
      todo.completed = !todo.completed;
    }
  }

  function removeTodo(id) {
    const index = todos.findIndex(t => t.id === id);
    if (index !== -1) {
      todos.splice(index, 1);
    }
  }

  function toggleAll() {
    const newState = !allCompleted;
    todos.forEach(t => t.completed = newState);
  }

  function clearCompleted() {
    todos = todos.filter(t => !t.completed);
  }

  // Effect for persistence
  $effect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  });

  // Load from localStorage on initialization
  $effect(() => {
    const saved = localStorage.getItem('todos');
    if (saved) {
      todos = JSON.parse(saved);
    }
  });
</script>

<div class="todo-app">
  <h1>My Tasks</h1>

  <form onsubmit={(e) => { e.preventDefault(); addTodo(); }}>
    <input
      type="text"
      bind:value={newTodo}
      placeholder="What needs to be done?"
    />
    <button type="submit">Add</button>
  </form>

  {#if todos.length > 0}
    <div class="controls">
      <button onclick={toggleAll}>
        {allCompleted ? 'Uncheck' : 'Check'} All
      </button>

      <div class="filters">
        <button
          class:active={filter === 'all'}
          onclick={() => filter = 'all'}
        >
          All
        </button>
        <button
          class:active={filter === 'active'}
          onclick={() => filter = 'active'}
        >
          Active
        </button>
        <button
          class:active={filter === 'completed'}
          onclick={() => filter = 'completed'}
        >
          Completed
        </button>
      </div>
    </div>

    <ul class="todo-list">
      {#each filteredTodos as todo (todo.id)}
        <li class:completed={todo.completed}>
          <input
            type="checkbox"
            checked={todo.completed}
            onchange={() => toggleTodo(todo.id)}
          />
          <span>{todo.text}</span>
          <button onclick={() => removeTodo(todo.id)}>X</button>
        </li>
      {/each}
    </ul>

    <footer>
      <span>{remainingCount} task(s) remaining</span>
      {#if todos.some(t => t.completed)}
        <button onclick={clearCompleted}>
          Clear Completed
        </button>
      {/if}
    </footer>
  {/if}
</div>

Why Runes Are Better?

1. Explicit Reactivity

In Svelte 4, any let variable was automatically reactive, which could cause confusion:

<!-- Svelte 4: When is this reactive? -->
<script>
  let data = fetch('/api').then(r => r.json()); // Reactive?
  let config = { theme: 'dark' }; // Reactive?
  const MAX = 100; // Not reactive (const)
</script>

With Runes, you explicitly declare what is reactive:

<!-- Svelte 5: Total clarity -->
<script>
  let data = $state(null); // Reactive
  let config = $state({ theme: 'dark' }); // Reactive
  const MAX = 100; // Not reactive (intentional)
</script>

2. Better TypeScript Support

Runes work perfectly with TypeScript:

<script lang="ts">
  interface Todo {
    id: number;
    text: string;
    completed: boolean;
  }

  let todos = $state<Todo[]>([]);
  let newTodo = $state('');

  // Types correctly inferred
  let completedCount = $derived(
    todos.filter(t => t.completed).length
  );
</script>

3. Universal Reactivity

Runes work outside .svelte components:

// stores/counter.svelte.ts
export function createCounter(initial = 0) {
  let count = $state(initial);

  return {
    get value() { return count; },
    increment() { count += 1; },
    decrement() { count -= 1; },
    reset() { count = initial; }
  };
}

// In any component
import { createCounter } from './stores/counter.svelte';

const counter = createCounter(10);

Comparison with React

Let us compare the same functionality in React and Svelte 5:

React (Hooks):

import { useState, useMemo, useEffect } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);

  // useMemo for derived values
  const doubled = useMemo(() => count * 2, [count]);

  // useEffect for side effects
  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]);

  return (
    <div>
      <p>Count: {count} (doubled: {doubled})</p>
      <input
        type="number"
        value={step}
        onChange={e => setStep(Number(e.target.value))}
      />
      <button onClick={() => setCount(c => c + step)}>
        Increment by {step}
      </button>
    </div>
  );
}

Svelte 5 (Runes):

<script>
  let count = $state(0);
  let step = $state(1);

  // Derived - no dependency array
  let doubled = $derived(count * 2);

  // Effect - no dependency array
  $effect(() => {
    document.title = `Count: ${count}`;
  });
</script>

<div>
  <p>Count: {count} (doubled: {doubled})</p>
  <input type="number" bind:value={step} />
  <button onclick={() => count += step}>
    Increment by {step}
  </button>
</div>

Notable differences:

  • No dependency arrays (fewer bugs)
  • More concise syntax
  • Native two-way binding
  • Less boilerplate

Svelte 5 Performance

Svelte 5 brings significant performance improvements:

Benchmarks (js-framework-benchmark)

Metric React 19 Vue 3.5 Svelte 5
Create 1000 rows 45ms 38ms 32ms
Update 1000 rows 38ms 32ms 28ms
Select row 3.2ms 2.8ms 2.1ms
Remove row 35ms 30ms 25ms
Create 10000 rows 450ms 380ms 320ms
Bundle size (gzip) 45KB 35KB 3KB

Highlight: Svelte compiles to vanilla JavaScript, resulting in significantly smaller bundles.

Migrating from Svelte 4 to Svelte 5

Migration Strategy

  1. Update Svelte: npm install svelte@5
  2. Existing code works: Svelte 5 is backward compatible
  3. Migrate gradually: Convert components one at a time
  4. Use legacy mode: <svelte:options runes={false} />

Migration Tool

Svelte offers an automatic migration tool:

# Migrate files automatically
npx sv migrate svelte-5

# Check compatibility issues
npx svelte-check

Migration Patterns

Stores to Runes:

// Svelte 4: Stores
import { writable, derived } from 'svelte/store';

export const count = writable(0);
export const doubled = derived(count, $count => $count * 2);

// Svelte 5: Runes (stores still work!)
// But Runes are recommended for new code
let count = $state(0);
let doubled = $derived(count * 2);

When to Use Svelte 5?

Use Svelte 5 when:

  • Starting a new frontend project
  • Needing maximum performance
  • Wanting less boilerplate
  • Preferring syntax closer to vanilla JavaScript
  • Needing smaller bundles

Consider other options when:

  • Team is very experienced in React/Vue
  • Library ecosystem is crucial
  • Needing complex SSR (SvelteKit is still younger)

Conclusion

Svelte 5 with Runes represents a significant evolution in how we think about reactivity in JavaScript. The explicit syntax, better TypeScript support, and superior performance make it an attractive choice for new projects.

If you already work with React or Vue, Runes concepts will be familiar, but the implementation is more elegant and straightforward. It is worth experimenting on a personal project to feel the difference.

The JavaScript community continues to innovate, and frameworks like Svelte show there is still much room for improvement in how we build web applications.

If you want to explore more about modern JavaScript frameworks, I recommend checking out the article on React vs Vue in 2025: Which Framework to Choose where we compare the main market options.

Let's go! 🦅

📚 Want to Deepen Your JavaScript Knowledge?

This article covered Svelte 5 and Runes, but there is much more to explore in modern development.

Developers who invest in solid, structured knowledge tend to have more opportunities in the market.

Complete Study Material

If you want to master JavaScript from basics to advanced, I have prepared a complete guide:

Investment options:

  • 1x of $4.90 on card
  • or $4.90 at sight

👉 Learn About JavaScript Guide

💡 Material updated with industry best practices

Comments (0)

This article has no comments yet 😢. Be the first! 🚀🦅

Add comments