Skip to main content

Examples

Fine-grained reactivity

In Svelte 4, reactivity centres on the component and the top-level state declared therein. What this means is that in a situation like this...

<script>
	let todos = [];

	function remaining(todos) {
		console.log('recalculating');
		return todos.filter((todo) => !todo.done).length;
	}

	function addTodo(event) {
		if (event.key !== 'Enter') return;

		let done = false;
		let text = event.target.value;

		todos = [
			...todos,
			{
				done,
				text
			}
		];

		event.target.value = '';
	}
</script>

<input on:keydown={addTodo} />

{#each todos as todo}
	<div>
		<input bind:value={todo.text} />
		<input type="checkbox" bind:checked={todo.done} />
	</div>
{/each}

<p>{remaining(todos)} remaining</p>

...editing any individual todo will invalidate the entire list. You can see this for yourself by opening the playground, adding some todos, and watching the console in the bottom right. remaining(todos) is recalculated every time we edit the text of a todo, even though it can't possibly affect the result.

Worse, everything inside the each block needs to be checked for updates. When a list gets large enough, this behaviour has the potential to cause performance headaches.

With runes, it's easy to make reactivity fine-grained, meaning that things will only update when they need to. Begin by using the $state rune:

<script>
	let todos = [];
	let todos = $state([]);

	function remaining(todos) {
		console.log('recalculating');
		return todos.filter(todo => !todo.done).length;
	}

	function addTodo(event) {
		if (event.key !== 'Enter') return;

		let done = false;
		let text = event.target.value;
		let done = $state(false);
		let text = $state(event.target.value);

		todos = [...todos, {
			done,
			text
		}];

		event.target.value = '';
	}
</script>

Next, update done and text to use get and set properties:

<script>
	let todos = $state([]);

	function remaining(todos) {
		console.log('recalculating');
		return todos.filter(todo => !todo.done).length;
	}

	function addTodo(event) {
		if (event.key !== 'Enter') return;

		let done = $state(false);
		let text = $state(event.target.value);

		todos = [...todos, {
			done,
			text
			get done() { return done },
			set done(value) { done = value },
			get text() { return text },
			set text(value) { text = value }
		}];

		event.target.value = '';
	}
</script>

In this version of the app, editing the text of a todo won't cause unrelated things to be updated.

Gotchas

If we only do the first step (adding $state) and skip the second (exposing the state via get and set properties), the app breaks — toggling the checkboxes won't cause remaining(todos) to be recalculated.

That's because in runes mode, Svelte no longer invalidates everything when you change something inside an each block. Previously, Svelte tried to statically determine the dependencies of the mutated value in order to invalidate them, causing confusing bugs related to overfiring (invalidating things that weren't actually affected) and underfiring (missing affected variables). It made apps slower by default and harder to reason about, especially in more complex scenarios.

In runes mode, the rules around triggering updates are simpler: Only state declared by a $state or $props rune causes a rerender. In the broken example, todo is declared by the #each block, and neither the text nor the done property are referencing values of $state runes. One solution would be to turn text and done into $state references, as shown above. The other solution would be to bind to todos[i].text instead of todo.text — this way, Svelte picks up the reference to the todos $state and invalidates it as a whole. Keep in mind that you lose the fine-grained reactivity this way — the whole array is invalidated on every keystroke.