bindingbindeachsvelte

Svelte each worked twice / each binding error ( Cannot set properties of undefined )


I'm trying to bind elements in {#each} block and removing them by click.

    <script>
        const foodList = [
          { icon: '🍲', elem: null, },
          { icon: '🥫', elem: null, },
          { icon: '🍔', elem: null, },
        ]; 
        const remove = (index) => { 
          foodList.splice(index, 1);
          foodList = foodList;
        };
    </script>

    {#each foodList as {icon, elem}, index}
      <div 
        bind:this={elems[index]}
        on:click={remove}
      >
        {icon}
      </div>
    {/each}

In my code i got 2 problems:

Why it works like this?


Solution

  • I'm writing this not for asking help, but for people, that will meet same problems

    Both of these problems have the same origin, so I gather them here:

    All code tested in Svelte compiler version 3.59.1 & 4.0.5


    Problem 1 - each worked twice

    What do I mean?

    Just let me show you, look at code below, how many iterations {#each} will do?

    <script>
      const foodList = [
            { icon: '🍲' },
            { icon: '🥫' },
            { icon: '🍔' } 
      ]; 
    </script>
    
    {#each foodList as {icon}}
      <div> {icon} </div>
    {/each}
    

    If your answer is - 3, then congrats, you are right.

    Ok, now we need to bind elements, that we are rendering in {#each} block.

    It's the same code, just added prop 'elem' to objects for each div binding inside {#each}

    Look below and try again, how many iterations will make {#each}?

    <script>
      const foodList = [
            { icon: '🍲', elem: null, },
            { icon: '🥫', elem: null, },
            { icon: '🍔', elem: null, } 
      ]; 
    </script>
    
    {#each foodList as {icon, elem} }
      <div 
        bind:this={elem}
       > 
         {icon} 
       </div>
    {/each}
    

    Right... we got 6 iterations, twice more.

    You can see it by adding some console.log() at first {#each} block code, like this:

    {#each foodList as {icon, elem}, index}
      {console.log('each iteration: ', index, icon) ? '' : ''}
      <div 
        bind:this={elem}
       > 
         {icon} 
       </div>
    {/each}
    

    It happens because we used same array for {#each} iterations and binding.

    If we will create new array for binding - the problem will be gone:

    <script>
      const foodList = [
            { icon: '🍲' },
            { icon: '🥫' },
            { icon: '🍔' } 
      ]; 
      const elems = [];
    </script>
    
    {#each foodList as {icon}, index }
      { console.log('each iteration: ', index, icon) ? '' : ''}
      <div 
        bind:this={elems[index]} 
       > 
         {icon} 
       </div>
    {/each}
    

    Yeah... now the problem is gone and we got 3 iterations, as we expected.
    It's a bug that lives for a long time, as I tried to find out - at least 1 year. This leads to different problems in different circumstances as out code become more complex.


    Problem 2 - each binding throws error after iterable array has been sliced

    (Something like: 'Cannot set properties of undefined')

    What do I mean?

    Same example, just added removing array item on it's element click:

    <script>
        const foodList = [
            { icon: '🍲', elem: null, },
            { icon: '🥫', elem: null, },
            { icon: '🍔', elem: null, }  
        ]; 
        
        const remove = (index) => { 
            foodList.splice(index, 1);
            foodList = foodList;
        };
    </script>
    
    {#each foodList as {icon, elem}, index} 
        {console.log('each iteration: ', index, icon) ? '' : ''} 
        <div 
            bind:this={elem}
            on:click={remove}
        >
            {icon}
        </div>
    {/each}
    

    We expect that if we make a click on div - array item, to which it was bound will be sliced and we will lose them on screen. Correct... but we got error, because {#each} block make still 3 iterations, not 2 as we were waiting for.

    Again for clearness, our code steps: foodList length is 3 -> we make a click on food icon -> foodList length is 2 (we cut item with clicked icon).

    After this {#each} should do 2 iterations to render each left icon in foodList, but he did 3!

    That's why we have the problem, our code trying to write new property to undefined (item is sliced so when we are trying to read\write it - there is undefined.

    // foodList [ { icon: '🍲', elem: null, }, { icon: '🥫', elem: null, }]
    foodList[2].elem = <div>; // "Cannot set properties of undefined"

    It's a bug and it happens if we used same array for {#each} iterations and binding.

    The most clean fix on my question is to separate iterable and binding data into different arrays:

    <script>
        const foodList = [
            { icon: '🍲' },
            { icon: '🥫' },
            { icon: '🍔' }  
        ]; 
        let elems = [];
        
        const remove = (index) => { 
            foodList.splice(index, 1);
            foodList = foodList;
        };
          
        
    </script>
    
    {#each foodList as {icon, cmp}, index} 
        {console.log('each iteration: ', index, icon) ? '' : ''} 
        <div 
            bind:this={elems[index]}
            on:click={remove}
        >
            {icon}
        </div>
    {/each}
    

    But... Let's look inside out new elems array by adding $: console.log(elems);

    (it's reactive expression, that will print elems array each time as it changes)

    <script>
        const foodList = [
            { icon: '🍲' },
            { icon: '🥫' },
            { icon: '🍔' }  
        ]; 
        let elems = [];
        
        const remove = (index) => { 
            foodList.splice(index, 1);
            foodList = foodList;
        };
          
        $: console.log(elems);  
    </script>
    
    {#each foodList as {icon, cmp}, index} 
        {console.log('each iteration: ', index, icon) ? '' : ''} 
        <div 
            bind:this={elems[index]}
            on:click={remove}
        >
            {icon}
        </div>
    {/each}
    

    Looks like a have 2 news for you

    It means that problem is still here( {#each} block makes still 1 extra iteration for sliced item).

    For now we can filter elems array after foodList slicing, just do it after page update, such as tick().

    Full code:

    <script>
        import { tick } from 'svelte';
        
        const foodList = [
            { icon: '🍲', elem: null, },
            { icon: '🥫', elem: null, },
            { icon: '🍔', elem: null, } 
        ]; 
        let elems = [];
        
        const remove = async (index) => { 
            foodList.splice(index, 1);
            foodList = foodList;
            await tick();
            elems = elems.filter((elem) => (elem !== null)); 
        };
        
        $: console.log(elems);  
    </script>
    
    {#each foodList as {icon, elem}, index}
        {console.log('each iteration: ', index, icon) ? '' : ''}
        <div 
            bind:this={elems[index]}
            on:click={remove}
        >
            {icon}
      </div>
    {/each}
    
    

    Keep in mind: {#each} block still works 1 extra time and we got null as bound elem, we just filtered it after the page updates.

    Last stand

    Don't know what to say for real... I wasted too much time on this **** trying to figure out why my code isn't work as it should be.

    I like svelte, but I don't like bugs

    I really hope this little guide will helps some of you to save a lot of time.

    Will be glad to your corrections, see you and don't let 🐞 win.

    P.S.

    Yep, it takes time, but... Never know when you will needs help, share your knowledge