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?
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
- Explicit Reactivity: Use
$state()for variables that mutate and need to update the DOM. - Never Write Manual Listeners for Derived Values: Replace manual recalculations with
$derived()or$derived.by(). - Always Clean Up Resources: Return teardown callbacks in
$effect()to prevent memory leaks and orphaned intervals. - Decouple Business Logic into
.svelte.ts: Keep components focused strictly on presentation while reactive classes manage system state.