Understanding Assignment and Reactivity in Svelte
Jesse Wei
Posted on January 30, 2023
Svelte's reactivity is built into the language as this post noted. Despite limited experiece with svelte yet, I feel more comfortable working with reactivity in svelte than in other frontend frameworks I've used so far.
In this post, I want to share a question I had on the topic and my effort to find the answer.
Table of Contents
My Question
When I first picked up svelte a month ago after working with React for a few years, I made a simple todo app to play with.
<script>
let todos = [
{id: 0, name: 'buy milk', done: false},
{id: 1, name: 'buy food', done: false}
]
function toggle() {
// TODO: implement toggle
}
</script>
<ul>
{#each todos as todo (todo.id)}
<li>
Name: {todo.name} Done: {todo.done}
</li>
{/each}
</ul>
<button on:click={toggle}>Toggle</button>
I tried implementing two versions of the toggle
function as the following and was surprised to see the different outcomes produced by them when I hit the button:
// version 1
function toggle() {
todos.forEach((todo, i) => {
todo.done = true
console.log(todos)
})
}
// version 2
function toggle() {
todos.forEach((_, i) => {
todos[i].done = true
console.log(todos)
})
}
Obviously, both toggle
s updated the data but only version 2 triggered an update of the UI, in which case we say, in the frontend development world, the app is reactive to toggle
version 2 but not version 1.
Why?
Assignment With Side-Effect
To answer the question, I realized I had first to understand how reactivity works in svelte.
According to the docs, svelte's reactivity is based on assignments. But what are the implications of this? Both of my implementations of the toggle
function include an assignment statement and a further check indicated that both todo
in version 1 and todos[i]
in version 2 point to the same object as expected:
function toggle() {
todos.forEach((todo, i) => {
console.log(todo === todos[i]) // => true
})
}
This explained why the data, todos
, was correctly updated in both cases as the console outputs showed and made it clear that there must have been a side-effect(as svelte creator Rich Harris mentioned in this video) coming along with the version 2 of toggle
that triggered a re-rendering of the UI.
$$invalidate and Indirect Assignment
One thing I love about svelte most is the highly interactive REPL. It makes playing around with the code much easier and of much more fun.
I checked out the compiled JS of both versions of the toggle
function and found they were not nearly the same:
// version 1: compiled
function toggle() {
todos.forEach((todo, i) => {
todo.done = true;
});
}
// version 2: compiled
function toggle() {
todos.forEach((_, i) => {
$$invalidate(0, todos[i].done = true, todos);
});
}
The alien-looking $$invalidate
function in the second snippet turned out to be the source of all the magic. It is the side-effect generated by assignment that triggers an update of the screen.
You don't need to know the implementation details of $$invalidate
to understand reactivity in svelte, but for those who are curious, feel free to take a look at this amazing post by svelte's maintainer Tan Li Hau before diving deep into the source code of svelte.
Now, the question becomes why did one assignment generate $$invalidate
and another fail to? The answer has something to do with indirect assignment.
In the official tutorial, it's pointed out that indirect assignments won't trigger reactivity.
Just as this code, used in the tutorial, won't trigger reactivity:
const foo = obj.foo;
foo.bar = 'baz';
The first implementation of toggle
won't either, because fundamentally, it is an indirect assignment:
// version 1
function toggle() {
todos.forEach((todo, i) => {
todo.done = true
})
}
// is equivalent to
function toggle() {
todos.forEach((_, i) => {
const todo = todos[i]
todo.done = true
})
}
Svelte checks if the updated variable or any of its properties is assigned a new value to determine whether $$invalidate
should be generated. This is how reactivity in svelte is implemented under the hood. If you wonder HOW it is implemented, again, go ahead and read its docs.
Based on what we've found, if I'm so stubborn as to insist on using the first version of toggle
but want all the reactive magic to happen at the same time, I can just add a legitimate assignment like the following to generate the $$invalidate
side-effect:
// version 1
function toggle() {
todos.forEach((todo, i) => {
todo.done = true
todos = todos // add this line
})
}
todos[i] = todos[i]
also does the trick:
// version 1
function toggle() {
todos.forEach((todo, i) => {
todo.done = true
todos[i] = todos[i] // this works too
})
}
Conclusion
The answer to my question turned out to be that the code, which failed to trigger a re-rendering of the UI, had an indirect assignment and indirect assignment does not trigger reactivity in svelte.
We looked at the $$invalidate
side-effect which is the source of reactive magic behind svelte. We realized that to make a variable reactive, we must assign a value to itself or its properties if the variable is an object, otherwise $$invalidate
won't be generated.
Posted on January 30, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 26, 2024
November 26, 2024