Source Code Repository
Fullstack Web Intermediate 25 min read 7 reads

Reactive State Architecture with Svelte 5 Runes

Lab Abstract & Architecture Objectives
Complete engineering guide to Svelte 5 Runes: fine-grained reactivity using $state, $derived, $effect, typed snippets, and universal reactive classes.
VI
Published on September 02, 2026

Reactive State Architecture with Svelte 5 Runes

Svelte 5 marks a major architectural milestone in frontend engineering. By replacing compiler-level assignment magic (let count = 0; count += 1;) and separate store modules (writable()) with Runes, Svelte delivers fine-grained signals that operate consistently both inside .svelte components and across standalone .svelte.ts business logic files.

This guide provides an end-to-end walkthrough of building scalable, reactive frontend architectures with Svelte 5 Runes.


1. The Paradigm Shift: Why Runes?

Svelte 5 Runes Reactive State Engine Architecture Figure 1: Svelte 5 fine-grained reactive graph: $state primary signals, $derived memoization, and surgical DOM updates vs legacy component re-renders.

In Svelte 3 and 4, reactivity was constrained to the top level of .svelte components. Moving reactive logic into pure TypeScript files required the Readable/Writable store subscription pattern with auto-subscription $store syntax that worked only inside template components.

Feature Svelte 4 Pattern Svelte 5 Rune Advantage
Reactive State let count = 0; let count = $state(0); Works anywhere in .svelte and .svelte.ts
Derived State $: double = count * 2; let double = $derived(count * 2); Explicit dependency tracking; pure memoization
Side Effects $: { console.log(count); } $effect(() => { ... }); Predictable lifecycle; explicit teardown callbacks
Component Props export let title = 'Default'; let { title } = $props(); Standard ES destructuring; fully typed
Template Composition <slot name="header" /> {#snippet header()} ... {/snippet} Type-safe parameter passing with {@render}

2. Core Reactive Primitives: $state & $state.raw

Fine-Grained Reactivity with $state()

$state wraps objects, arrays, and primitive values with deep reactive proxies. Mutating a nested property triggers updates only for DOM nodes subscribed to that exact field:

<script lang="ts">
    interface TelemetryNode {
        id: string;
        metrics: { cpu: number; memory: number };
        status: 'ONLINE' | 'DEGRADED' | 'OFFLINE';
    }

    // Deeply reactive state
    let server = $state<TelemetryNode>({
        id: "edge-ke-nbo-01",
        metrics: { cpu: 14.2, memory: 42.8 },
        status: "ONLINE"
    });

    function updateCpu(val: number) {
        // Direct in-place mutation - only updates the CPU meter element!
        server.metrics.cpu = val;
    }
</script>

<div class="card">
    <h3>Node: {server.id}</h3>
    <p>CPU Load: {server.metrics.cpu}%</p>
    <button onclick={() => updateCpu(+(Math.random() * 100).toFixed(1))}>
        Simulate Tick
    </button>
</div>

High-Throughput Performance with $state.raw()

When dealing with massive immutable collections (such as WebSocket streaming buffers or WebGL vertices), deep proxying introduces unnecessary memory and CPU overhead. Use $state.raw():

// Only re-triggers subscribers when the array reference itself is replaced
let dataStream = $state.raw<Float64Array>(new Float64Array(10000));

3. Pure Derivations with $derived & $derived.by

Derived state computes read-only values based on active $state dependencies. Svelte memoizes the result and re-evaluates only when input dependencies change.

<script lang="ts">
    interface Item { id: number; name: string; price: number; quantity: number }

    let items = $state<Item[]>([
        { id: 1, name: "NVMe Drive 2TB", price: 180, quantity: 2 },
        { id: 2, name: "ECC RAM 64GB", price: 240, quantity: 4 },
    ]);

    // Simple single-expression derivation
    let itemCount = $derived(items.reduce((acc, it) => acc + it.quantity, 0));

    // Multi-line derivation using $derived.by
    let subtotal = $derived.by(() => {
        return items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
    });

    let vatAmount = $derived(subtotal * 0.16);
    let orderTotal = $derived(subtotal + vatAmount);
</script>

<p>Items: {itemCount} | Total: ${orderTotal.toFixed(2)} (VAT: ${vatAmount.toFixed(2)})</p>

4. Side Effects & Resource Teardown with $effect

$effect runs in the browser after the DOM has been rendered and synchronized with reactive state. It automatically tracks any state read inside its closure.

Effect Teardown & Lifecycle Cleanup

<script lang="ts">
    let pollIntervalMs = $state(5000);
    let latestLog = $state("Awaiting telemetry...");

    $effect(() => {
        // Runs whenever pollIntervalMs changes
        console.log(`[Telemetry] Starting polling interval at ${pollIntervalMs}ms`);

        const timer = setInterval(async () => {
            const res = await fetch('/api/v1/metrics/');
            const json = await res.json();
            latestLog = `Received telemetry packet: ${json.timestamp}`;
        }, pollIntervalMs);

        // CLEANUP: Returned function runs before the effect re-runs and on component destroy
        return () => {
            console.log("[Telemetry] Cleaning up previous interval timer.");
            clearInterval(timer);
        };
    });
</script>

5. Universal Reactivity: Standalone Reactive Classes

One of Svelte 5's greatest strengths is that runes work in standard .svelte.ts files. You can encapsulate complete domain logic into testable, reusable classes:

// stores/telemetry.svelte.ts
export class ClusterTelemetryStore {
    // Reactive private state
    #nodes = $state<Map<string, { status: string; load: number }>>(new Map());
    #connectionState = $state<'DISCONNECTED' | 'CONNECTING' | 'CONNECTED'>('DISCONNECTED');

    constructor(private clusterUrl: string) {}

    // Public derived getters
    get connected() {
        return this.#connectionState === 'CONNECTED';
    }

    get activeNodeCount() {
        let count = 0;
        for (const [_, node] of this.#nodes) {
            if (node.status === 'ONLINE') count++;
        }
        return count;
    }

    get nodeList() {
        return Array.from(this.#nodes.entries()).map(([id, data]) => ({ id, ...data }));
    }

    // Public actions
    connect() {
        this.#connectionState = 'CONNECTING';
        // Connect websocket or event source...
    }

    recordHeartbeat(nodeId: string, status: string, load: number) {
        this.#nodes.set(nodeId, { status, load });
    }
}

6. Typed Props and Snippets: The Modern Component API

Replace legacy <slot /> tags with type-safe Snippets:

<!-- components/DataGrid.svelte -->
<script lang="ts" generics="T">
    import type { Snippet } from 'svelte';

    interface Props {
        data: T[];
        title: string;
        rowSnippet: Snippet<[item: T, index: number]>;
        headerSnippet?: Snippet;
    }

    let { data, title, rowSnippet, headerSnippet }: Props = $props();
</script>

<div class="datagrid">
    <header>
        <h2>{title}</h2>
        {#if headerSnippet}
            {@render headerSnippet()}
        {/if}
    </header>

    <div class="rows">
        {#each data as item, index}
            <div class="grid-row">
                {@render rowSnippet(item, index)}
            </div>
        {/each}
    </div>
</div>

7. Summary & Architectural Takeaways

  1. Explicit Reactivity: Use $state() for variables that mutate and need to update the DOM.
  2. Never Write Manual Listeners for Derived Values: Replace manual recalculations with $derived() or $derived.by().
  3. Always Clean Up Resources: Return teardown callbacks in $effect() to prevent memory leaks and orphaned intervals.
  4. Decouple Business Logic into .svelte.ts: Keep components focused strictly on presentation while reactive classes manage system state.
Topic Tags: #Svelte 5 #Runes #TypeScript #Frontend #Fullstack #Architecture
Back to top
VI
About the Author

Victor Owino

Executive Lead for Fullstack Engineering. Crafting modern user interfaces with Svelte 5, React, and seamless backend API integrations.