Using Svelte 5 Runes for Fine‑Grained Reactive State
Learn how Svelte 5’s $state, $derived, and $effect runes replace the $: label syntax, enable module‑scope reactive state, and reduce unnecessary re‑renders in large UIs.
08 Jan 2026, 12:24 UTC

Problem: Reactive logic scattered across components
In a Svelte 4 project you often see statements like $: total = price * quantity or $: if (count > 0) { … } inside components. As the UI grows, any change to a dependency causes the compiler to mark the whole component subtree for update, which can lead to unnecessary work in list‑heavy views.
Thesis: Svelte 5’s runes ($state, $derived, $effect) replace the label syntax with compiler‑generated signals that update only the DOM nodes that actually depend on changed data, and they let you declare reactive state at module scope without a separate store.
How the runes work
The Svelte 5 compiler transforms each rune into a low‑level signal primitive:
$statecreates a reactive cell that tracks reads and writes.$derivedcreates a memoized computation that re‑runs only when its tracked$statedependencies change.$effectschedules a side‑effect to run after the DOM has been updated, taking the place ofonMountandafterUpdatefor reactive logic.
Because the dependency graph is fine‑grained, updating a single item in a large list does not cause the whole list component to re‑render.
Worked example: Shared cart state
Suppose you want a shopping cart that any component can read and update, without creating a dedicated store file.
- Create a module‑scope file
src/lib/cart.js(the file can be.js,.ts, or.svelte.js– the runes are recognized by the compiler):
// src/lib/cart.js
import { /* no import needed for runes */ } from 'svelte';
// Reactive state holding an array of line items
export const cart = $state([]);
// Derived total price
// $derived takes a function that returns the value
export const total = $derived(() => {
return cart.reduce((sum, item) => sum + item.price * item.qty, 0);
});
// Helper to add an item
// Mutating the array via push is tracked, but we must reassign the whole array
// so the proxy sees a change (deep property changes are tracked, array methods are not).
export function addItem(item) {
cart.push({ ...item, qty: 1 });
// Reassign to trigger update
cart = [...cart];
}
- Use the cart in a component:
// src/routes/+page.svelte
{#each cart as item}
- {item.name} – ${item.price} × {item.qty}
{/each}
Total: ${total}
addItem({ name: 'Apple', price: 0.5 })>Add Apple
When the button is clicked, only the {#each} block and the total binding update; the rest of the page stays untouched.
Trade‑off and limitation
The main consideration is how array mutations are tracked. The proxy deep‑tracks object property changes, but it does not intercept array mutation methods like push, splice, or sort. Therefore, after calling cart.push(...) you must reassign the array (cart = [...cart]) or use a method that returns a new array (cart = cart.concat(newItem)) to signal a change. Object property updates such as cart[0].qty = 2 are tracked automatically and will update dependents without reassignment.
To verify this behavior:
- Run the app in dev mode (
npm run dev). - Open the browser console and, assuming you have imported the cart as
cart, execute:
// Assuming cart is imported
cart[0].qty = 99; // UI updates immediately
cart.push({ name: 'Banana', price: 0.3, qty: 1 }); // UI does NOT update yet
cart = [...cart]; // UI updates after reassignment
Check that the text bound to {cart[0].qty} changes after the first line, and that the list only grows after the reassignment.
Actionable closing
Start by extracting one piece of shared state (e.g., a UI theme or form validation) into a module‑scope $state. Import it where needed and observe that components react instantly without extra store boilerplate. Then replace remaining $: blocks with $derived for calculations and $effect for side‑effects. Monitor the compiled output in .svelte-kit/output/client/**/*.js for calls to create_signal, create_derived, and create_effect to confirm the compiler is emitting signal‑based code. This approach gives you fine‑grained updates, clearer type inference, and a smoother path to server‑side rendering in SvelteKit.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.