Mastering Svelte 5 Runes: Building Reactive Web Apps with Modern Syntax

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

Shifting Paradigms: From Compiler Magic to Runes

For a long time, Svelte stood out because of its simplicity. We used let count = 0; and the compiler handled the rest. However, as applications grew, managing state across files using Writable stores became a bit of a boilerplate nightmare. I often found myself juggling the $ prefix for subscriptions and worrying about memory leaks when I forgot to unsubscribe in non-component files.

Svelte 5 changes the game with Runes. Instead of relying on top-level variable declarations that only work inside .svelte files, Runes provide a signals-based reactivity system that works everywhere.

This moves Svelte closer to how modern frameworks like Solid or Vue handle reactivity, but with a much cleaner syntax. I have applied this approach in production and the results have been consistently stable, especially when dealing with complex data structures that need to be shared across multiple modules.

The Traditional Way vs. The Runes Way

In Svelte 4, reactivity was scoped to the component. If you wanted to compute a value, you used the $: label. If you wanted to share state, you reached for svelte/store. Here is a quick comparison of how the code looks now:

// Svelte 4 - Component specific
let count = 0;
$: doubled = count * 2;

// Svelte 5 - Works anywhere
let count = $state(0);
let doubled = $derived(count * 2);

The biggest shift is that $state and $derived are now explicit. We no longer rely on the compiler guessing which variables are reactive. This clarity makes debugging significantly easier because you can trace exactly where a reactive value originated.

Pros and Cons of the Runes Approach

Transitioning to a new syntax always involves trade-offs. After refactoring several internal tools to Svelte 5, I’ve identified where this shines and where it might trip you up.

The Advantages

  • Universal Reactivity: You can define reactive state in .js or .ts files. You are no longer forced to keep logic inside component blocks just to keep it reactive.
  • No More Auto-subscriptions: The $store syntax is gone. You just access the variable directly. This removes a lot of cognitive load when reading code.
  • Fine-grained Updates: Svelte 5 uses signals under the hood, meaning only the specific part of the DOM that changes gets updated, rather than re-running larger chunks of component logic.
  • Unified Lifecycle: $effect replaces onMount, afterUpdate, and onDestroy with a single, more predictable mechanism.

The Challenges

  • Breaking Changes: If you have a large Svelte 4 codebase, the migration isn’t just a search-and-replace. You need to rethink how your data flows.
  • Verbosity: Writing $state(0) is slightly more typing than let count = 0. Some developers might find the explicit runes a bit noisy at first.

Recommended Setup for Svelte 5

If you want to start experimenting with Runes, I recommend setting up a fresh Vite project. Since Svelte 5 is the current direction of the ecosystem, the tooling is already quite mature.

# Create a new Svelte project
npm create vite@latest my-svelte-app -- --template svelte-ts

# Navigate into the directory
cd my-svelte-app

# Install dependencies
npm install

# Ensure you are on the latest Svelte version
npm install svelte@next

In your svelte.config.js, you don’t need any special configuration to enable Runes; they are part of the core library now. However, I always suggest enabling TypeScript to get the most out of the type-safety that Runes provide, especially when defining $props().

Implementation Guide: Building a Reactive App

Let’s look at how to implement a basic reactive system using the four core Runes: $state, $derived, $props, and $effect.

1. Managing State with $state

Instead of simple assignments, use $state for any data that should trigger UI updates. This works for primitives, arrays, and objects.

<script>
  let user = $state({
    name: 'Engineer',
    tasks: []
  });

  function addTask(task) {
    user.tasks.push(task); // Svelte 5 tracks this automatically!
  }
</script>

2. Derived Values with $derived

Whenever you need a value that depends on another piece of state, $derived is your go-to. It replaces the old $: reactive declarations. I find this much cleaner because it behaves like a normal variable but stays in sync.

<script>
  let items = $state([10, 20, 30]);
  let total = $derived(items.reduce((a, b) => a + b, 0));
</script>

<p>The sum is: {total}</p>

3. Component Communication with $props

Passing data into components used to involve export let name;. In Svelte 5, we use $props(). This feels more like modern JavaScript destructuring and allows for better default value handling.

<script>
  let { title, status = 'pending' } = $props();
</script>

<h1>{title}</h1>
<span>Status: {status}</span>

4. Handling Side Effects with $effect

This is where things get interesting. $effect replaces almost all lifecycle hooks. It runs whenever the reactive values inside it change. It’s great for logging, syncing with local storage, or fetching data when an ID changes.

<script>
  let userId = $state(1);

  $effect(() => {
    console.log(`User ID changed to: ${userId}`);
    
    // Cleanup logic (replaces onDestroy)
    return () => {
      console.log('Cleaning up before the next run or component destruction');
    };
  });
</script>

5. Replacing Stores with Shared State

The real “pro move” in Svelte 5 is creating a shared state file without using svelte/store. You can simply export an object containing $state values. I’ve found this to be the most stable way to manage global state in my recent production deployments.

// logger.js (or .ts)
export const appState = $state({
  theme: 'dark',
  toggleTheme() {
    this.theme = this.theme === 'dark' ? 'light' : 'dark';
  }
});

Then, in any component, you just import appState and use it directly. No $ prefix, no manual subscriptions, and no boilerplate.

Final Thoughts

Runes represent a significant evolution for Svelte. While the shift from the compiler-heavy approach to an explicit reactivity system might feel like a big change, the benefits in terms of code reusability and clarity are undeniable. My team noticed a decrease in “reactive bugs”—those weird moments where a variable wouldn’t update because the compiler didn’t catch the dependency—almost immediately after switching to Runes.

If you are starting a new project today, I highly recommend embracing Svelte 5 from the beginning. It makes the development experience much more predictable and aligns Svelte with the future of the web ecosystem.

Share: