Skip to main content

Examples

Old vs new

This page intends to give a broad overview of how code written using runes looks compared to code not using them. You will see that for most simple tasks that only involve a single component it will actually not look much different. For more complex logic, runes simplify things.

Counter

The $state, $derived and $effect runes replace magic let declarations, $: x = ... and $: { ... }.

<script>
	let count = 0;
	$: double = count * 2;

	$: {
	let count = $state(0);
	let double = $derived(count * 2);
	$effect(() => {
		if (count > 10) {
			alert('Too high!');
		}
	}
	});
</script>

<button on:click={() => count++}>
	{count} / {double}
</button>

Tracking dependencies

In non-runes mode, dependencies of $: statements are tracked at compile time. a and b changing will only cause sum to be recalculated because the expression — add(a, b) — refers to those values.

In runes mode, dependencies are tracked at run time. sum will be recalculated whenever a or b change, whether they are passed in to the add function or accessed via closure. This results in more maintainable, refactorable code.

<script>
	let a = 0;
	let b = 0;
	$: sum = add(a, b);
	let a = $state(0);
	let b = $state(0);
	let sum = $derived(add());

	function add(a, b) {
	function add() {
		return a + b;
	}
</script>

<button on:click={() => a++}>a++</button>
<button on:click={() => b++}>b++</button>
<p>{a} + {b} = {sum}</p>

Untracking dependencies

Conversely, suppose you — for some reason — wanted to recalculate sum when a changes, but not when b changes.

In non-runes mode, we 'hide' the dependency from the compiler by excluding it from the $: statement. In runes mode, we have a better and more explicit solution: untrack.

<script>
	import { untrack } from 'svelte';

	let a = 0;
	let b = 0;
	$: sum = add(a);
	let a = $state(0);
	let b = $state(0);
	let sum = $derived(add());

	function add(a) {
		return a + b;
	function add() {
		return a + untrack(() => b);
	}
</script>

<button on:click={() => a++}>a++</button>
<button on:click={() => b++}>b++</button>
<p>{a} + {b} = {sum}</p>

Simple component props

<script>
	export let count = 0;
	let { count = 0 } = $props();
</script>

{count}

Advanced component props

<script>
	let classname = '';
	export { classname as class };
	let { class: classname, ...others } = $props();
</script>

<pre class={classname}>
	{JSON.stringify($$restProps)}
	{JSON.stringify(others)}
</pre>

Autoscroll

To implement a chat window that autoscrolls to the bottom when new messages appear (but only if you were already scrolled to the bottom), we need to measure the DOM before we update it.

In Svelte 4, we do this with beforeUpdate, but this is a flawed approach — it fires before every update, whether it's relevant or not. In the example below, we need to introduce checks like updatingMessages to make sure we don't mess with the scroll position when someone toggles dark mode.

With runes, we can use $effect.pre, which behaves the same as $effect but runs before the DOM is updated. As long as we explicitly reference messages inside the effect body, it will run whenever messages changes, but not when theme changes.

beforeUpdate, and its equally troublesome counterpart afterUpdate, will be deprecated in Svelte 5.

<script>
	import { beforeUpdate, afterUpdate, tick } from 'svelte';
	import { tick } from 'svelte';

	let updatingMessages = false;
	let theme = 'dark';
	let messages = [];
	let theme = $state('dark');
	let messages = $state([]);

	let div;

	beforeUpdate(() => {
	$effect.pre(() => {
		if (!updatingMessages) return;
		messages;
		const autoscroll = div && div.offsetHeight + div.scrollTop > div.scrollHeight - 50;

		if (autoscroll) {
			tick().then(() => {
				div.scrollTo(0, div.scrollHeight);
			});
		}

		updatingMessages = false;
	});

	function handleKeydown(event) {
		if (event.key === 'Enter') {
			const text = event.target.value;
			if (!text) return;

			updatingMessages = true;
			messages = [...messages, text];
			event.target.value = '';
		}
	}

	function toggle() {
		toggleValue = !toggleValue;
	}
</script>

<div class:dark={theme === 'dark'}>
	<div bind:this={viewport}>
		{#each messages as message}
			<p>{message}</p>
		{/each}
	</div>

	<input on:keydown={handleKeydown} />

	<button on:click={toggle}>
		Toggle dark mode
	</button>
</div>