Mero
Mero

Reputation: 763

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?

Upvotes: 1

Views: 1023

Answers (1)

Mero
Mero

Reputation: 763

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:

  • some times {#each} block works twice more than expected
  • some times {#each} block binding throw an error

All code tested in Svelte compiler version 3.59.1 & 4.0.5


Problem 1 - each worked twice

What do I mean?

  • each block makes all iterations twice

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?

  • each block binding throws error after we removed one of iterable array items

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

  • we got no error
  • we have new null item in elems array

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

Upvotes: 8

Related Questions