Skip to main content
PORTFOLIO

Mastering Svelte 5 Runes: A Paradigm Shift in Reactivity

Mohit Byadwal

Mastering Svelte 5 Runes: A Paradigm Shift in Reactivity

Svelte 5 replaces the implicit “magic” of let + $: with explicit runes: small compiler directives that make where reactivity lives obvious in your source. For teams shipping large apps, that shift is not cosmetic—it changes how you structure stores, effects, and component APIs.

From implicit reactivity to compile-time signals

Earlier Svelte inferred reactive statements from assignments. Runes flip the model: you opt in per binding with $state for mutable reactive values and $derived for values computed from other reactive state. The compiler can trace dependencies statically, which tends to produce tighter update graphs and fewer accidental dependencies than broad $: blocks.

<script>
	let count = $state(0);
	const doubled = $derived(count * 2);

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

<button onclick={increment}>{count}</button>
<p>{doubled}</p>

Here, doubled updates only when count changes—no manual subscription APIs, and no risk of a stale closure unless you deliberately capture a non-reactive snapshot.

Side effects with $effect (and when not to use it)

$effect runs after the DOM updates when its reactive dependencies change. It is the rune-era analogue of “do something when X changes,” but you should still treat it like useEffect in React: prefer event handlers, $derived, and lifecycle-style APIs for things that don’t need post-render observation.

<script>
	let query = $state('');
	let results = $state([]);

	$effect(() => {
		const q = query;
		if (!q) {
			results = [];
			return;
		}
		const controller = new AbortController();
		fetch(`/api/search?q=${encodeURIComponent(q)}`, { signal: controller.signal })
			.then((r) => r.json())
			.then((data) => (results = data))
			.catch(() => {});
		return () => controller.abort();
	});
</script>

Patterns like abortable fetch, DOM measurement, and third-party imperative widgets are where $effect shines. For pure data transforms, keep logic in $derived or plain functions over reactive values.

Component contracts: $props and snippet slots

$props() formalizes inputs. Combined with snippets (the evolution of slots), you get typed, explicit component boundaries—critical for design systems and route-level composition.

<script>
	let { title, children } = $props();
</script>

<section>
	<h2>{title}</h2>
	{@render children?.()}
</section>

SEO angle: clearer component APIs reduce duplication; less duplicated markup and logic often means leaner HTML payloads and more consistent heading hierarchy—both indirect ranking signals when paired with good Core Web Vitals.

Migration and mental models

Practical migration usually proceeds in layers: convert leaf components first, replace $: with $derived, move imperative “watches” into $effect with explicit cleanup, and revisit global stores—many patterns map cleanly to module-level $state or contextual providers.

Bottom line: runes are not “another hooks API.” They are compiler-guided reactivity that makes dependencies readable, debuggable, and optimizable—exactly what mature frontends need as apps grow past the prototype stage.