Understanding Assignment and Reactivity in Svelte

jessewei

Jesse Wei

Posted on January 30, 2023

Understanding Assignment and Reactivity in Svelte

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>
Enter fullscreen mode Exit fullscreen mode

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) 
  })
}
Enter fullscreen mode Exit fullscreen mode

Output for toggle version 1

Output for toggle version 1

Output for toggle version 2

Output for toggle version 2

Obviously, both toggles 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
  })
}
Enter fullscreen mode Exit fullscreen mode

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);
  });
}
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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
  })
}
Enter fullscreen mode Exit fullscreen mode

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
  })
}
Enter fullscreen mode Exit fullscreen mode

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
  })
}
Enter fullscreen mode Exit fullscreen mode

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.

šŸ’– šŸ’Ŗ šŸ™… šŸš©
jessewei
Jesse Wei

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