When having an #each block in svelte (like https://learn.svelte.dev/tutorial/keyed-each-blocks), the entry is only updated if the content changed. This works perfectly in the tutorials example, where a string is given as property to a nested component:
{#each things as thing (thing.id)}
<Thing name={thing.name}/>
{/each}
But if I give the whole object (thing
) and adjust Thing accordingly, it always updates all list entries. Hence I wonder what the condition is Svelte decides on, whether to update the component or not. Is it the property, which is incase of the whole object a reference and therefore always changes? Or is the whole Nested component generated to be compared against the DOM? Is it bad practice to give an Object to a component?
App.svelte
<script>
import Thing from './Thing.svelte';
let things = [
{ id: 1, name: 'apple' },
{ id: 2, name: 'banana' },
{ id: 3, name: 'carrot' },
{ id: 4, name: 'doughnut' },
{ id: 5, name: 'egg' }
];
function handleClick() {
things = things.slice(1);
}
</script>
<button on:click={handleClick}>
Remove first thing
</button>
{#each things as thing (thing.id)}
<Thing name={thing} />
{/each}
Thing.svelte
<script>
import {
beforeUpdate,
afterUpdate
} from 'svelte';
const emojis = {
apple: '🍎',
banana: '🍌',
carrot: '🥕',
doughnut: '🍩',
egg: '🥚'
};
export let name;
const emoji = emojis[name.name];
beforeUpdate(() => {
console.log('before updating ' + name.id)
});
afterUpdate(() => {
console.log('after updating ' + name.id)
});
</script>
<p>{emoji} = {name.name}</p>
The update lifecycle functions are called everytime, even if the content didn’t change.
Edit:
With the REPL there is this JS output tab which I searched a little. There are many of these p() {…} like:
p(ctx, [dirty]) {
if (dirty & /*name*/ 1 && t2_value !== (t2_value = /*name*/ ctx[0].name + "")) set_data(t2, t2_value);
},
which seem to do the job. The one above is the one from the Thing
create_fragment
return. To me, the comparison seems good, but still an update is done.
2
Answers
You have a couple of questions in your post, so to best help, I’ll break them down into separate parts. I hope this clears things up at least a little bit.
Part 1: Why does the component update when I pass in the whole object?
It sounds like you are on the right track. This is how Svelte’s reactive behavior works. It will trigger an update when the props are determined as changed; however, it does not do a deep equality check. So in your example, you are passing in a whole object, so the reference to the object will be used to determine if the prop has changed.
In the
{#each}
block, you have set{thing.id}
as the key. This means that Svelte will follow these rules (for the most part) to determine whether it should rerender the component:thing.id
changes.So here comes the tricky part. Even though you are only slicing out part of the array in
handleClick()
Svelte will still update the component since it sees that the reference has changed.Note: Creating a new array with new references is the behavior of
slice
.To get around this, you could pass the component a specific property of the object rather than the entire thing:
Part 2: Is giving an object to a component bad practice?
This is a bit subjective, but in my opinion, passing an object is not necessarily a bad practice, but you should be aware of the implications. A couple of high-level points off the top of my head might be:
But like I said, this is only my opinion.
Part 3:
p(ctx, [dirty])
and what is going on hereAs I’m sure you know, this is a part of Svelte’s compiled code. This checks if the
name
prop in thectx
object (current context of the component) has changed, and if so, it will update the text content of the corresponding DOM element. In your case, thename
prop is an object, and every time you slice the array, that object’s reference changes, so this function will always consider it as "changed".The most important thing to understand is that Svelte works via invalidation and that by default a lot gets invalidated, so changes are less likely to be missed.
At the same time, just because something gets invalidated does not mean that any DOM update happens. If the data used in the UI did not change, Svelte has no reason to do anything.
A change to an array or its element invalidates the array as a whole, this then propagates to any elements or components that use the array or its elements. If this were not the case, a change to single item (e.g.
things[0].name = 'banana'
) would not work as expected as both array and element are still referencing the same object.An invalidation will incur some necessary checking work (that bit of compiled code you extracted) but this is not very expensive as just the used properties are checked and the UI is only touched if something actually changed.
If you know that an object passed to the component is not changed (i.e. its properties are not modified), you can use
<svelte:options immutable />
in the component to signal that to the compiler. It then will not invalidate the component if the reference stays the same. In your example adding this toThing
results in zero updates when removing an item (REPL).For the most part you should not need to worry about this but if you actually run into performance issues this is one way to optimize the invalidation checks.