4

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.

1
  • What do you even mean by "updates all list entries"? Also, show your code. Commented Jul 16, 2023 at 18:03

2 Answers 2

2

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?

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?

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:

  1. The object is new (it was not on the list before).
  2. The object with the same key (thing.id) has been removed from the list.
  3. The key 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:

{#each things as thing (thing.id)}
    <Thing name={thing.name}/>
{/each}

Part 2: Is giving an object to a component bad practice?

Is it bad practice to give an Object to a component?

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:

  1. If the object is large or changes often, this could lead to many unnecessary updates and performance problems.
  2. If only specific properties of the object are used by the component, passing only those properties could improve performance and make the code more transparent.

But like I said, this is only my opinion.

Part 3: p(ctx, [dirty]) and what is going on here

The update lifecycle functions are called every time, even if the content didn't change.

As I'm sure you know, this is a part of Svelte's compiled code. This checks if the name prop in the ctx 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, the name 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".

Sign up to request clarification or add additional context in comments.

2 Comments

For objects (i.e. not primitives), Svelte will consider them changed even if the reference did not change unless the immutable option is set. Otherwise changes to properties of objects would get lost.
Thank you very much for the detailed explanation. Part 1 makes sense to me. Part 2 aswell, I was just sometimes a bit lazy and forwarded what I fetched, guess I will change it where appropriate. Part 3 still confuses me a bit, because I think there are multiple p() calls and this is probaly the last one. The previous likely compare the reference, but this one has ctx[0].name, where ctx[0] is the object, if I'm not wrong.
1

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. beforeUpdate/afterUpdate callbacks will run regardless of whether any UI changes happen as soon as a component has been marked as dirty and goes through an update loop.

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 to Thing 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.

7 Comments

Thanks a lot for the reply and the REPL. This invalidation explains it. You mentioned that: ` this is not very expensive as just the used properties are checked and the UI is only touched if something actually changed.` Does that mean that the actual comparison t2_value !== (t2_value = /*name*/ ctx[0].name + "" evals true even if the object contains the same name content?
No, that will be false since this does a string comparison. If the name did effectively not change, nothing happens. If it did change, t2, the text node showing the name, will be updated using set_data. The check is performed even if the thing is the same, the UI is only updated if thing.name actually changed.
That makes sense. Hence a UI update is avoided even if an object is used, just at the very last comparison call (p(), maybe I got the wrong idea?). But If it evals to false, why do the lifecycle functions run? Aren't they supposed to run on a UI update (schedules work to happen immediately before the DOM is updated)?
The callbacks run whenever the update loop runs, regardless of whether anything happens within. For the before-case it would be impossible to know anyway.
This explains the inner working perfectly, I would love to take it as accepted answer. Basically, somewhere dirty_components is filled and on flush all $$.before_update and p() are called. Independent of whether p() evals true, $$.after_update.forEach(add_render_callback); is done, adding the afterUpdate to a list for later callback (the foreEach is a bit strange, are there multiple afterUpdate per fragment?). My conclusion is that objects are not bad, it just gets a little bit more dirty but no UI redrawings are triggered if not needed.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.