Reactivity

Reactive assignments

The reactivity system introduced in Svelte 3 has made it easier than ever to trigger updates to the DOM. Despite this, there are a few simple rules that you must always follow. This guide explains how Svelte’s reactivity system works, what you can and cannot do, as well a few pitfalls to avoid.

Top-level variables

The simplest way to make your Svelte components reactive is by using an assignment operator. Any time Svelte sees an assignment to a top-level variable an update is scheduled. A ‘top-level variable’ is any variable that is defined inside the script element but is not a child of anything, meaning, it is not inside a function or a block. Incidentally, these are also the only variables that you can reference in the DOM. Let’s look at some examples.

The following works as expected and update the dom:

<script>
	let num = 0;

	function updateNum() {
		num = 25;
	}
</script>

<button on:click={updateNum}>Update</button>

<p>{num}</p>

Svelte can see that there is an assignment to a top-level variable and knows to re-render after the num variable is modified.

each blocks

From Svelte 3.23.1 onwards the following issue no longer applies. You can now assign to array item primitives and the changes will be reflected in the original array. See this REPL running on Svelte 3.23.1, where the issue has been fixed. And this REPL running on Svelte 3.23.0 where the issue still applies. What follows is for archival purposes for anyone on Svelte 3.23.0 and below. For further context see Svelte issue 4744.

The only exception to the top-level variable rule is when you are inside an each block. Any assignments to variables inside an each block trigger an update. Only assignments to array items that are objects or arrays result in the array itself updating. If the array items are primitives, the change is not traced back to the original array. This is because Svelte only reassigns the actual array items and primitives are passed by value in javascript, not by reference.

The following example causes the array and, subsequently, the DOM to be updated:

<script>
	let list = [{ n: 1 }, { n: 2 }, { n: 3 }];
</script>

{#each list as item}
	<button on:click={() => (item.n *= 2)}>{item.n}</button>
{/each}

This, however, will not:

<script>
	let list = [1, 2, 3];
</script>

{#each list as item}
	<button on:click={() => (item *= 2)}>{item}</button>
{/each}

The easiest workaround is to just reference the array item by index from inside the each block:

<script>
	let list = [1, 2, 3];
</script>

{#each list as item, index}
	<button on:click={() => (list[index] *= 2)}>{item}</button>
{/each}

Variables not values

Svelte only cares about which variables are being reassigned, not the values to which those variables refer. If the variable you reassign is not defined at the top level, Svelte does not trigger an update, even if the value you are updating updates the original variable’s value as well.

This is the sort of problem you may run into when dealing with objects. Since objects are passed by reference and not value, you can refer to the same value in many different variables. Let’s look at an example:

<script>
	let obj = {
		num: 0
	};

	function updateNum() {
		const o = obj;
		o.num = 25;
	}
</script>

<button on:click={updateNum}>Update</button>

<p>{obj.num}</p>

In this example, when we reassign o.num we are updating the value assigned to obj but since we are not updating the actual obj variable Svelte does not trigger an update. Svelte does not trace these kinds of values back to a variable defined at the top level and has no way of knowing if it has updated or not. Whenever you want to update the local component state, any reassignments must be performed on the actual variable, not just the value itself.

Shadowed variables

Another situation that can sometimes cause unexpected results is when you reassign a function’s parameter (as above), and that parameter has the same name as a top-level variable.

<script>
	let obj = {
		num: 0
	};

	function updateNum(obj) {
		obj.num = 25;
	}
</script>

<button on:click={() => updateNum(obj)}>Update</button>

<p>{num}</p>

This example behaves the same as the previous example, except it is perhaps even more confusing. In this case, the obj variable is being shadowed while inside the function, so any assignments to obj inside this function are assignments to the function parameter rather than the top-level obj variable. It refers to the same value, and it has the same name, but it is a different variable inside the function scope.

Reassigning function parameters in this way is the same as reassigning a variable that points back to the top-level variable’s value and does not cause an update. To avoid these problems, and potential confusion, it is a good idea not to reuse variable names in different scopes (such as inside functions), and always make sure that you are reassigning a top-level variable.

Reactive Declarations

In addition to the assignment-based reactivity system, Svelte also has special syntax to define code that should rerun when its dependencies change using labeled statements - $:.

<script>
	let n = 0;

	$: n_squared = n * n;
</script>

<button on:click={() => (n += 1)}>{n_squared}</button>

Whenever Svelte sees a reactive declaration, it makes sure to execute any reactive statements that depend on one another in the correct order and only when their direct dependencies have changed. A ‘direct dependency’ is a variable that is referenced inside the reactive declaration itself. References to variables inside functions that a reactive declaration calls are not considered dependencies.

<script>
	let n = 0;

	const squareIt = () => n * n;

	$: n_squared = squareIt();
</script>

<button on:click={() => (n += 1)}>{n_squared}</button>

In the above example, n_squared will not be recalculated when n changes because Svelte is not looking inside the squareIt function to define the reactive declaration’s dependencies.

Defining dependencies

Sometimes you want to rerun a reactive declaration when a value changes but the variable itself is not required (in the case of some side-effects). The solution to this involves listing the dependency inside the declaration in some way.

The simplest solution is to pass the variable as an argument to a function, even if that variable is not required.

let my_value = 0;

function someFunc() {
	console.log(my_value);
}

$: someFunc(my_value);

In this example, someFunc doesn’t require the my_value argument, but this lets Svelte know that my_value is a dependency of this reactive declaration and should rerun whenever it changes. In general, it makes sense to use any parameters that are passed in, even though all of these variables are in scope, it can make the code easier to follow.

let my_value = 0;

function someFunc(value) {
	console.log(value);
}

$: someFunc(my_value);

Here we have refactored someFunc to take an argument, making the code easier to follow.

If you need to list multiple dependencies and passing them as arguments is not an option, then the following pattern can be used:

let one;
let two;
let three;

const someFunc = () => sideEffect();

$: one, two, three, someFunc();

This simple expression informs Svelte that it should rerun whenever one, two, or three changes. This expression runs the line of code whenever these dependencies change.

Similarly, if you only want to rerun a function when a variable changes and is truthy then you can use the following pattern:

let one;

const someFunc = () => sideEffect();

$: one && someFunc();

If the value of one is not truthy, then this short circuit expression stops at one and someFunc is not executed.

Variable deconstruction

Assigning a new value in a reactive declaration essentially turns these declarations into computed variables. However, deconstructing an object or array inside a reactive declaration may seem verbose or even impossible at first glance.

since $: const { value } = object is not valid javascript, we need to take a different approach. You can deconstruct values using reactive declarations by forcing javascript to interpret the statement as an expression.

To achieve this, all we need to do is wrap the declaration with parentheses:

let some_obj = { one: 1, two: 2, three: 3 };

$: ({ one, two, three } = some_obj);

Javascript interprets this as an expression, deconstructing the value as expected with no unnecessary code required.

Hiding values from reactive declarations

On ocassion, you may wish to run a reactive declaration when only some of its dependencies change, in essence, you need to hide specific dependencies from Svelte. This is the opposite case of declaring additional dependencies and can be achieved by not referencing the dependencies in the reactive declaration but hiding those references in a function.

let one;
let two;

const some_func = (n) => console.log(n * two);

$: some_func(one);

The reactive declaration in this example reruns only when one changes, we have hidden the reference to two inside a function because Svelte does not look inside of referenced functions to track dependencies.

Published:
Last update: